From 8b415ee015702271d73c41542532fbee27ef1cd7 Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 10:57:34 +0800 Subject: [PATCH 01/12] feat: index source map --- src/cached_source.rs | 61 +++++- src/concat_source.rs | 341 +++++++++++++++++++++++++++++++- src/lib.rs | 4 +- src/source.rs | 462 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 862 insertions(+), 6 deletions(-) diff --git a/src/cached_source.rs b/src/cached_source.rs index 2562c636..1bb5c168 100644 --- a/src/cached_source.rs +++ b/src/cached_source.rs @@ -12,7 +12,7 @@ use crate::{ stream_chunks_of_source_map, Chunks, GeneratedInfo, StreamChunks, }, object_pool::ObjectPool, - source::SourceValue, + source::{IndexSourceMap, SourceValue}, BoxSource, MapOptions, RawBufferSource, Source, SourceExt, SourceMap, }; @@ -23,6 +23,8 @@ struct CachedData { chunks: OnceLock>, columns_map: OnceLock>, line_only_map: OnceLock>, + columns_index_map: OnceLock>, + line_only_index_map: OnceLock>, } /// It tries to reused cached results from other methods to avoid calculations, @@ -155,6 +157,26 @@ impl Source for CachedSource { } } + fn index_map( + &self, + object_pool: &ObjectPool, + options: &MapOptions, + ) -> Option { + if options.columns { + self + .cache + .columns_index_map + .get_or_init(|| self.inner.index_map(object_pool, options)) + .clone() + } else { + self + .cache + .line_only_index_map + .get_or_init(|| self.inner.index_map(object_pool, options)) + .clone() + } + } + fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { self.inner.to_writer(writer) } @@ -477,4 +499,41 @@ mod tests { let cached_size = cached.size(); assert_eq!(raw_size, cached_size); } + + #[test] + fn index_map_should_be_cached() { + let original = OriginalSource::new("hello\nworld\n", "test.txt"); + let cached = CachedSource::new(original); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + + let index_map1 = cached.index_map(&pool, &options); + let index_map2 = cached.index_map(&pool, &options); + assert!(index_map1.is_some()); + assert_eq!(index_map1, index_map2); + } + + #[test] + fn index_map_cached_matches_inner() { + let inner = ConcatSource::new([ + OriginalSource::new("a\n", "a.js").boxed(), + OriginalSource::new("b\n", "b.js").boxed(), + ]); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + let expected = inner.index_map(&pool, &options); + let cached = CachedSource::new(inner); + let result = cached.index_map(&pool, &options); + assert_eq!(result, expected); + } + + #[test] + fn index_map_returns_none_for_raw_cached() { + let cached = CachedSource::new(RawStringSource::from("no map")); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + assert!(cached.index_map(&pool, &options).is_none()); + // Second call also returns None (from cache) + assert!(cached.index_map(&pool, &options).is_none()); + } } diff --git a/src/concat_source.rs b/src/concat_source.rs index 8a7ce39a..1a7ed84d 100644 --- a/src/concat_source.rs +++ b/src/concat_source.rs @@ -11,7 +11,7 @@ use crate::{ helpers::{get_map, Chunks, GeneratedInfo, StreamChunks}, linear_map::LinearMap, object_pool::ObjectPool, - source::{Mapping, OriginalLocation}, + source::{IndexSourceMap, Mapping, OriginalLocation, Section, SectionOffset}, BoxSource, MapOptions, RawStringSource, Source, SourceExt, SourceMap, SourceValue, }; @@ -216,6 +216,100 @@ impl Source for ConcatSource { result } + fn index_map( + &self, + object_pool: &ObjectPool, + options: &MapOptions, + ) -> Option { + let children = self.optimized_children(); + + if children.len() == 1 { + return children[0].index_map(object_pool, options); + } + + let mut sections = Vec::new(); + let mut current_line_offset: u32 = 0; + let mut current_column_offset: u32 = 0; + + for child in children { + // Get the index map for this child (may itself have sections) + if let Some(child_index_map) = child.index_map(object_pool, options) { + for section in child_index_map.sections() { + // Offset the section by the current position + let line = section.offset.line + current_line_offset; + let column = if section.offset.line == 0 { + section.offset.column + current_column_offset + } else { + section.offset.column + }; + sections.push(Section { + offset: SectionOffset { line, column }, + map: section.map.clone(), + }); + } + } + + // Calculate generated info to determine offset for the next child. + // We iterate via `rope()` to avoid allocating the full source string. + let mut line_count: u32 = 0; + let mut last_line_column: u32 = 0; + let mut ends_with_newline = true; + child.rope(&mut |chunk| { + for byte in chunk.as_bytes() { + if *byte == b'\n' { + line_count += 1; + last_line_column = 0; + ends_with_newline = true; + } else { + ends_with_newline = false; + // Count UTF-16 code units for column offset. + // ASCII bytes (< 0x80): 1 UTF-16 unit (only count leading bytes) + // We only need to count leading bytes of multi-byte sequences. + if (*byte & 0xC0) != 0x80 { + if *byte < 0x80 { + last_line_column += 1; + } else if *byte < 0xF0 { + // 2 or 3 byte sequence -> 1 UTF-16 code unit + last_line_column += 1; + } else { + // 4 byte sequence -> 2 UTF-16 code units (surrogate pair) + last_line_column += 2; + } + } + } + } + }); + + if child.size() == 0 { + // Empty child doesn't change the offset + continue; + } + + // Update offsets for next child, matching ConcatSource's concat logic. + // generated_line is like line_count + 1 (1-based), or line_count + 1 if + // ends with newline (extra empty line). + let generated_line = if ends_with_newline { + line_count + 1 + } else { + line_count.max(1) + }; + let generated_column = if ends_with_newline { 0 } else { last_line_column }; + + if generated_line > 1 || line_count > 0 { + current_column_offset = generated_column; + } else { + current_column_offset += generated_column; + } + current_line_offset += line_count; + } + + if sections.is_empty() { + None + } else { + Some(IndexSourceMap::new(sections)) + } + } + fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { for child in self.optimized_children() { child.to_writer(writer)?; @@ -493,6 +587,251 @@ mod tests { use super::*; + #[test] + fn index_map_returns_none_for_only_raw_sources() { + let source = ConcatSource::new([ + RawStringSource::from("Hello World\n").boxed(), + RawStringSource::from("Bye\n").boxed(), + ]); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + assert!(source.index_map(&pool, &options).is_none()); + } + + #[test] + fn index_map_single_original_source_child() { + let source = ConcatSource::new([OriginalSource::new( + "console.log('test');\n", + "test.js", + ) + .boxed()]); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + let index_map = source.index_map(&pool, &options).unwrap(); + // Single child -> delegates to child's index_map (1 section, offset 0,0) + assert_eq!(index_map.sections().len(), 1); + assert_eq!(index_map.sections()[0].offset.line, 0); + assert_eq!(index_map.sections()[0].offset.column, 0); + // The flattened source map should equal the child's map + let map = source.map(&pool, &options).unwrap(); + assert_eq!(index_map.to_source_map().unwrap(), map); + } + + #[test] + fn index_map_concat_two_original_sources() { + let source = ConcatSource::new([ + OriginalSource::new("line1\n", "a.js").boxed(), + OriginalSource::new("line2\n", "b.js").boxed(), + ]); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + let index_map = source.index_map(&pool, &options).unwrap(); + assert_eq!(index_map.sections().len(), 2); + // First section at 0,0 + assert_eq!(index_map.sections()[0].offset.line, 0); + assert_eq!(index_map.sections()[0].offset.column, 0); + // Second section at line 1 (after "line1\n") + assert_eq!(index_map.sections()[1].offset.line, 1); + assert_eq!(index_map.sections()[1].offset.column, 0); + + // Flattened should match regular map + let flat = index_map.to_source_map().unwrap(); + let map = source.map(&pool, &options).unwrap(); + assert_eq!(flat.sources(), map.sources()); + assert_eq!(flat.sources_content(), map.sources_content()); + } + + #[test] + fn index_map_with_raw_prefix() { + // RawStringSource (no map) followed by OriginalSource (has map) + let source = ConcatSource::new([ + RawStringSource::from("// header\n").boxed(), + OriginalSource::new( + "console.log('test');\nconsole.log('test2');\n", + "console.js", + ) + .boxed(), + ]); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + let index_map = source.index_map(&pool, &options).unwrap(); + // Only one section (from the OriginalSource), offset by 1 line + assert_eq!(index_map.sections().len(), 1); + assert_eq!(index_map.sections()[0].offset.line, 1); + assert_eq!(index_map.sections()[0].offset.column, 0); + } + + #[test] + fn index_map_with_raw_suffix() { + // OriginalSource followed by RawStringSource + let source = ConcatSource::new([ + OriginalSource::new("hello\n", "a.js").boxed(), + RawStringSource::from("// footer\n").boxed(), + ]); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + let index_map = source.index_map(&pool, &options).unwrap(); + assert_eq!(index_map.sections().len(), 1); + assert_eq!(index_map.sections()[0].offset.line, 0); + assert_eq!(index_map.sections()[0].offset.column, 0); + } + + #[test] + fn index_map_same_line_concat() { + // Two sources on the same line (no trailing newline in first) + let source = ConcatSource::new([ + OriginalSource::new("hello", "a.js").boxed(), + OriginalSource::new(" world", "b.js").boxed(), + ]); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + let index_map = source.index_map(&pool, &options).unwrap(); + assert_eq!(index_map.sections().len(), 2); + // First at 0,0 + assert_eq!(index_map.sections()[0].offset.line, 0); + assert_eq!(index_map.sections()[0].offset.column, 0); + // Second at 0,5 (same line, column 5 = length of "hello") + assert_eq!(index_map.sections()[1].offset.line, 0); + assert_eq!(index_map.sections()[1].offset.column, 5); + } + + #[test] + fn index_map_mixed_raw_and_original_sources() { + let source = ConcatSource::new([ + RawStringSource::from("Hello World\n").boxed(), + OriginalSource::new( + "console.log('test');\nconsole.log('test2');\n", + "console.js", + ) + .boxed(), + OriginalSource::new("Hello2\n", "hello.md").boxed(), + ]); + let pool = ObjectPool::default(); + let options = MapOptions::new(false); + let index_map = source.index_map(&pool, &options).unwrap(); + + // Two sections (from the two OriginalSources) + assert_eq!(index_map.sections().len(), 2); + + // First OriginalSource starts after "Hello World\n" -> line offset 1 + assert_eq!(index_map.sections()[0].offset.line, 1); + assert_eq!(index_map.sections()[0].offset.column, 0); + + // Second OriginalSource starts after the first one's 2 lines + // "Hello World\n" (1 line) + "console.log('test');\nconsole.log('test2');\n" (2 lines) = 3 lines + assert_eq!(index_map.sections()[1].offset.line, 3); + assert_eq!(index_map.sections()[1].offset.column, 0); + } + + #[test] + fn index_map_to_source_map_matches_regular_map() { + // Comprehensive test: the flattened IndexSourceMap should produce + // equivalent mappings to the regular map() method + let mut source = ConcatSource::new([ + RawStringSource::from("Hello World\n".to_string()).boxed(), + OriginalSource::new( + "console.log('test');\nconsole.log('test2');\n", + "console.js", + ) + .boxed(), + ]); + source.add(OriginalSource::new("Hello2\n", "hello.md")); + + let pool = ObjectPool::default(); + let options = MapOptions::new(false); + + let regular_map = source.map(&pool, &options).unwrap(); + let index_map = source.index_map(&pool, &options).unwrap(); + let flat_map = index_map.to_source_map().unwrap(); + + // Sources should match + assert_eq!(flat_map.sources(), regular_map.sources()); + assert_eq!(flat_map.sources_content(), regular_map.sources_content()); + + // Decoded mappings should match + let regular_mappings: Vec = + regular_map.decoded_mappings().collect(); + let flat_mappings: Vec = flat_map.decoded_mappings().collect(); + assert_eq!(regular_mappings.len(), flat_mappings.len()); + for (r, f) in regular_mappings.iter().zip(flat_mappings.iter()) { + assert_eq!(r.generated_line, f.generated_line); + assert_eq!(r.generated_column, f.generated_column); + assert_eq!( + r.original.as_ref().map(|o| o.source_index), + f.original.as_ref().map(|o| o.source_index) + ); + assert_eq!( + r.original.as_ref().map(|o| o.original_line), + f.original.as_ref().map(|o| o.original_line) + ); + assert_eq!( + r.original.as_ref().map(|o| o.original_column), + f.original.as_ref().map(|o| o.original_column) + ); + } + } + + #[test] + fn index_map_nested_concat_source() { + // Nested ConcatSource should flatten sections + let inner = ConcatSource::new([ + OriginalSource::new("a\n", "a.js").boxed(), + OriginalSource::new("b\n", "b.js").boxed(), + ]); + let outer = ConcatSource::new([ + inner.boxed(), + OriginalSource::new("c\n", "c.js").boxed(), + ]); + + let pool = ObjectPool::default(); + let options = MapOptions::default(); + let index_map = outer.index_map(&pool, &options).unwrap(); + + // Inner concat should contribute 2 sections, outer adds 1 = 3 total + assert_eq!(index_map.sections().len(), 3); + + // Verify offsets + assert_eq!(index_map.sections()[0].offset.line, 0); + assert_eq!(index_map.sections()[0].offset.column, 0); + assert_eq!(index_map.sections()[1].offset.line, 1); // after "a\n" + assert_eq!(index_map.sections()[1].offset.column, 0); + assert_eq!(index_map.sections()[2].offset.line, 2); // after "a\n" + "b\n" + assert_eq!(index_map.sections()[2].offset.column, 0); + + // Verify sources + assert_eq!(index_map.sections()[0].map.sources(), &["a.js".to_string()]); + assert_eq!(index_map.sections()[1].map.sources(), &["b.js".to_string()]); + assert_eq!(index_map.sections()[2].map.sources(), &["c.js".to_string()]); + + // Flattened should match regular map + let regular_map = outer.map(&pool, &options).unwrap(); + let flat_map = index_map.to_source_map().unwrap(); + assert_eq!(flat_map.sources(), regular_map.sources()); + let regular_mappings: Vec = + regular_map.decoded_mappings().collect(); + let flat_mappings: Vec = flat_map.decoded_mappings().collect(); + assert_eq!(regular_mappings.len(), flat_mappings.len()); + for (r, f) in regular_mappings.iter().zip(flat_mappings.iter()) { + assert_eq!(r.generated_line, f.generated_line); + assert_eq!(r.generated_column, f.generated_column); + } + } + + #[test] + fn index_map_with_empty_children() { + let source = ConcatSource::new([ + OriginalSource::new("hello\n", "a.js").boxed(), + RawStringSource::from("").boxed(), + OriginalSource::new("world\n", "b.js").boxed(), + ]); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + let index_map = source.index_map(&pool, &options).unwrap(); + assert_eq!(index_map.sections().len(), 2); + assert_eq!(index_map.sections()[0].offset.line, 0); + assert_eq!(index_map.sections()[1].offset.line, 1); + } + #[test] fn should_concat_two_sources() { let mut source = ConcatSource::new([ diff --git a/src/lib.rs b/src/lib.rs index c5dca46a..d566b5a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,8 +23,8 @@ pub use original_source::OriginalSource; pub use raw_source::{RawBufferSource, RawStringSource}; pub use replace_source::{ReplaceSource, ReplacementEnforce}; pub use source::{ - BoxSource, MapOptions, Mapping, OriginalLocation, Source, SourceExt, - SourceMap, SourceValue, + BoxSource, IndexSourceMap, MapOptions, Mapping, OriginalLocation, Section, + SectionOffset, Source, SourceExt, SourceMap, SourceValue, }; pub use source_map_source::{ SourceMapSource, SourceMapSourceOptions, WithoutOriginalOptions, diff --git a/src/source.rs b/src/source.rs index 92ee366c..98f00ac5 100644 --- a/src/source.rs +++ b/src/source.rs @@ -11,7 +11,7 @@ use dyn_clone::DynClone; use serde::{Deserialize, Serialize}; use crate::{ - helpers::{decode_mappings, Chunks, StreamChunks}, + helpers::{decode_mappings, encode_mappings, Chunks, StreamChunks}, object_pool::ObjectPool, Result, }; @@ -130,6 +130,25 @@ pub trait Source: options: &MapOptions, ) -> Option; + /// Get the [IndexSourceMap]. + /// + /// Returns an index source map which uses sections to represent the mappings. + /// This is more efficient for concatenated sources as it avoids the expensive + /// mapping merge. The default implementation wraps the result of [Source::map] + /// into a single-section [IndexSourceMap]. + fn index_map( + &self, + object_pool: &ObjectPool, + options: &MapOptions, + ) -> Option { + self.map(object_pool, options).map(|map| { + IndexSourceMap::new(vec![Section { + offset: SectionOffset { line: 0, column: 0 }, + map, + }]) + }) + } + /// Update hash based on the source. fn update_hash(&self, state: &mut dyn Hasher) { self.dyn_hash(state); @@ -169,6 +188,15 @@ impl Source for BoxSource { self.as_ref().map(object_pool, options) } + #[inline] + fn index_map( + &self, + object_pool: &ObjectPool, + options: &MapOptions, + ) -> Option { + self.as_ref().index_map(object_pool, options) + } + #[inline] fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { self.as_ref().to_writer(writer) @@ -573,6 +601,229 @@ impl TryFrom for SourceMap { } } +/// The offset of a section within the generated code. +/// +/// Both `line` and `column` are 0-based, as specified by the +/// [Index Source Map](https://tc39.es/ecma426/#sec-index-source-map) format. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct SectionOffset { + /// 0-based line offset in the generated code. + pub line: u32, + /// 0-based column offset in the generated code. + pub column: u32, +} + +/// A section within an [IndexSourceMap], pairing an [offset](SectionOffset) +/// with a regular [SourceMap]. +/// +/// See [Index Source Map § Section](https://tc39.es/ecma426/#sec-index-source-map). +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct Section { + /// The offset in the generated code where this section begins. + pub offset: SectionOffset, + /// The source map for this section. + pub map: SourceMap, +} + +/// An [Index Source Map](https://tc39.es/ecma426/#sec-index-source-map) +/// that represents concatenated source maps as a list of [Section]s. +/// +/// Each section contains a regular [SourceMap] and an offset indicating +/// where that section starts in the generated output. This avoids the +/// need to merge mappings from multiple sources, improving performance +/// for concatenated sources like [ConcatSource](crate::ConcatSource). +/// +/// Use [IndexSourceMap::to_source_map] to flatten it into a regular [SourceMap]. +#[derive(Clone, PartialEq, Eq, Serialize)] +pub struct IndexSourceMap { + version: u8, + #[serde(skip_serializing_if = "Option::is_none")] + file: Option>, + sections: Vec
, +} + +impl std::fmt::Debug for IndexSourceMap { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { + write!( + f, + "IndexSourceMap {{ version: {}, file: {:?}, sections: {:?} }}", + self.version, self.file, self.sections + ) + } +} + +impl Hash for IndexSourceMap { + fn hash(&self, state: &mut H) { + self.file.hash(state); + self.sections.hash(state); + } +} + +impl IndexSourceMap { + /// Create a new [IndexSourceMap] from a list of [Section]s. + pub fn new(sections: Vec
) -> Self { + Self { + version: 3, + file: None, + sections, + } + } + + /// Get the file field. + pub fn file(&self) -> Option<&str> { + self.file.as_deref() + } + + /// Set the file field. + pub fn set_file>>(&mut self, file: Option) { + self.file = file.map(Into::into); + } + + /// Get the sections. + pub fn sections(&self) -> &[Section] { + &self.sections + } + + /// Flatten this [IndexSourceMap] into a regular [SourceMap] by merging + /// all sections, offsetting their mappings accordingly. + pub fn to_source_map(&self) -> Option { + if self.sections.is_empty() { + return None; + } + + // Single section with zero offset — return its map directly. + if self.sections.len() == 1 { + let section = &self.sections[0]; + if section.offset.line == 0 && section.offset.column == 0 { + let mut map = section.map.clone(); + if self.file.is_some() { + map.set_file(self.file.clone()); + } + return Some(map); + } + } + + let mut global_sources: Vec = Vec::new(); + let mut global_sources_content: Vec> = Vec::new(); + let mut global_names: Vec = Vec::new(); + let mut source_mapping: std::collections::HashMap = + std::collections::HashMap::new(); + let mut name_mapping: std::collections::HashMap = + std::collections::HashMap::new(); + + let mut all_mappings: Vec = Vec::new(); + + for section in &self.sections { + let map = §ion.map; + + // Build local-to-global source index mapping + let local_source_mapping: Vec = map + .sources() + .iter() + .enumerate() + .map(|(i, source)| { + if let Some(&idx) = source_mapping.get(source) { + // Update source content if we have better content + if let Some(content) = map.get_source_content(i) { + if (idx as usize) < global_sources_content.len() + && global_sources_content[idx as usize].is_empty() + { + global_sources_content[idx as usize] = content.clone(); + } + } + idx + } else { + let idx = global_sources.len() as u32; + source_mapping.insert(source.clone(), idx); + global_sources.push(source.clone()); + while global_sources_content.len() < global_sources.len() { + global_sources_content.push("".into()); + } + if let Some(content) = map.get_source_content(i) { + global_sources_content[idx as usize] = content.clone(); + } + idx + } + }) + .collect(); + + // Build local-to-global name index mapping + let local_name_mapping: Vec = map + .names() + .iter() + .map(|name| { + if let Some(&idx) = name_mapping.get(name) { + idx + } else { + let idx = global_names.len() as u32; + name_mapping.insert(name.clone(), idx); + global_names.push(name.clone()); + idx + } + }) + .collect(); + + // Decode, offset, and remap mappings + for mapping in map.decoded_mappings() { + // Offset the generated position. + // generated_line is 1-based; section.offset.line is 0-based. + let generated_line = mapping.generated_line + section.offset.line; + let generated_column = if mapping.generated_line == 1 { + mapping.generated_column + section.offset.column + } else { + mapping.generated_column + }; + + let original = mapping.original.map(|orig| OriginalLocation { + source_index: *local_source_mapping + .get(orig.source_index as usize) + .unwrap_or(&orig.source_index), + original_line: orig.original_line, + original_column: orig.original_column, + name_index: orig.name_index.map(|ni| { + *local_name_mapping + .get(ni as usize) + .unwrap_or(&ni) + }), + }); + + all_mappings.push(Mapping { + generated_line, + generated_column, + original, + }); + } + } + + if all_mappings.is_empty() { + return None; + } + + let mappings_str = encode_mappings(all_mappings.into_iter()); + let mut result = + SourceMap::new(mappings_str, global_sources, global_sources_content, global_names); + if self.file.is_some() { + result.set_file(self.file.clone()); + } + Some(result) + } + + /// Generate index source map to a JSON string. + pub fn to_json(&self) -> Result { + let json = simd_json::serde::to_string(&self)?; + Ok(json) + } + + /// Generate index source map to writer. + pub fn to_writer(self, w: W) -> Result<()> { + simd_json::serde::to_writer(w, &self)?; + Ok(()) + } +} + /// Represent a [Mapping] information of source map. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Mapping { @@ -636,7 +887,7 @@ mod tests { use std::collections::HashMap; use crate::{ - CachedSource, ConcatSource, OriginalSource, RawBufferSource, + CachedSource, ConcatSource, ObjectPool, OriginalSource, RawBufferSource, RawStringSource, ReplaceSource, SourceMapSource, WithoutOriginalOptions, }; @@ -795,4 +1046,211 @@ mod tests { "ab" ); } + + #[test] + fn index_source_map_serialization() { + let map = SourceMap::new( + "AAAA;AACA", + vec!["file.js".into()], + vec!["line1\nline2\n".into()], + vec![], + ); + let index_map = IndexSourceMap::new(vec![Section { + offset: SectionOffset { line: 0, column: 0 }, + map, + }]); + let json = index_map.to_json().unwrap(); + assert!(json.contains("\"version\":3")); + assert!(json.contains("\"sections\"")); + assert!(json.contains("\"offset\"")); + assert!(json.contains("\"map\"")); + } + + #[test] + fn index_source_map_to_source_map_single_section() { + let map = SourceMap::new( + "AAAA;AACA", + vec!["file.js".into()], + vec!["line1\nline2\n".into()], + vec![], + ); + let index_map = IndexSourceMap::new(vec![Section { + offset: SectionOffset { line: 0, column: 0 }, + map: map.clone(), + }]); + let result = index_map.to_source_map().unwrap(); + assert_eq!(result, map); + } + + #[test] + fn index_source_map_to_source_map_empty_sections() { + let index_map = IndexSourceMap::new(vec![]); + assert!(index_map.to_source_map().is_none()); + } + + #[test] + fn index_source_map_to_source_map_with_offset() { + // First section at line 0, col 0 + let map1 = SourceMap::new( + "AAAA", + vec!["a.js".into()], + vec!["hello\n".into()], + vec![], + ); + // Second section at line 1, col 0 (after the first line) + let map2 = SourceMap::new( + "AAAA", + vec!["b.js".into()], + vec!["world\n".into()], + vec![], + ); + let index_map = IndexSourceMap::new(vec![ + Section { + offset: SectionOffset { line: 0, column: 0 }, + map: map1, + }, + Section { + offset: SectionOffset { line: 1, column: 0 }, + map: map2, + }, + ]); + let result = index_map.to_source_map().unwrap(); + assert_eq!(result.sources(), &["a.js".to_string(), "b.js".to_string()]); + assert_eq!( + result.sources_content(), + &[Arc::from("hello\n"), Arc::from("world\n")] + ); + // Verify mappings: first mapping at line 1 (1-based), second at line 2 + let mappings: Vec = result.decoded_mappings().collect(); + assert_eq!(mappings.len(), 2); + assert_eq!(mappings[0].generated_line, 1); + assert_eq!(mappings[0].generated_column, 0); + assert_eq!( + mappings[0].original.as_ref().unwrap().source_index, + 0 + ); + assert_eq!(mappings[1].generated_line, 2); + assert_eq!(mappings[1].generated_column, 0); + assert_eq!( + mappings[1].original.as_ref().unwrap().source_index, + 1 + ); + } + + #[test] + fn index_source_map_to_source_map_with_column_offset() { + // First section at line 0, col 0 + let map1 = SourceMap::new( + "AAAA", + vec!["a.js".into()], + vec!["hello".into()], + vec![], + ); + // Second section at line 0, col 5 (same line, after "hello") + let map2 = SourceMap::new( + "AAAA", + vec!["b.js".into()], + vec!["world".into()], + vec![], + ); + let index_map = IndexSourceMap::new(vec![ + Section { + offset: SectionOffset { line: 0, column: 0 }, + map: map1, + }, + Section { + offset: SectionOffset { line: 0, column: 5 }, + map: map2, + }, + ]); + let result = index_map.to_source_map().unwrap(); + let mappings: Vec = result.decoded_mappings().collect(); + assert_eq!(mappings.len(), 2); + assert_eq!(mappings[0].generated_line, 1); + assert_eq!(mappings[0].generated_column, 0); + assert_eq!(mappings[1].generated_line, 1); + assert_eq!(mappings[1].generated_column, 5); + } + + #[test] + fn index_map_default_impl_wraps_map() { + let source = OriginalSource::new("hello\nworld\n", "test.txt"); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + let map = source.map(&pool, &options).unwrap(); + let index_map = source.index_map(&pool, &options).unwrap(); + + assert_eq!(index_map.sections().len(), 1); + assert_eq!(index_map.sections()[0].offset.line, 0); + assert_eq!(index_map.sections()[0].offset.column, 0); + assert_eq!(index_map.sections()[0].map, map); + } + + #[test] + fn index_map_returns_none_for_raw_source() { + let source = RawStringSource::from("hello world"); + let pool = ObjectPool::default(); + let options = MapOptions::default(); + assert!(source.index_map(&pool, &options).is_none()); + } + + #[test] + fn index_map_file_field_propagated() { + let map = SourceMap::new( + "AAAA", + vec!["a.js".into()], + vec!["hello".into()], + vec![], + ); + let mut index_map = IndexSourceMap::new(vec![Section { + offset: SectionOffset { line: 0, column: 0 }, + map, + }]); + index_map.set_file(Some("bundle.js")); + assert_eq!(index_map.file(), Some("bundle.js")); + + let result = index_map.to_source_map().unwrap(); + assert_eq!(result.file(), Some("bundle.js")); + } + + #[test] + fn index_source_map_shared_sources_across_sections() { + // Both sections reference the same source file + let map1 = SourceMap::new( + "AAAA", + vec!["shared.js".into()], + vec!["content".into()], + vec![], + ); + let map2 = SourceMap::new( + "AAAA", + vec!["shared.js".into()], + vec!["content".into()], + vec![], + ); + let index_map = IndexSourceMap::new(vec![ + Section { + offset: SectionOffset { line: 0, column: 0 }, + map: map1, + }, + Section { + offset: SectionOffset { line: 1, column: 0 }, + map: map2, + }, + ]); + let result = index_map.to_source_map().unwrap(); + // Should deduplicate sources + assert_eq!(result.sources().len(), 1); + assert_eq!(result.sources()[0], "shared.js"); + // Both mappings should reference source index 0 + let mappings: Vec = result.decoded_mappings().collect(); + assert_eq!( + mappings[0].original.as_ref().unwrap().source_index, + 0 + ); + assert_eq!( + mappings[1].original.as_ref().unwrap().source_index, + 0 + ); + } } From f0c5e326bf884fb9ebe2ee8bc1f30eabf97784bb Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 10:58:45 +0800 Subject: [PATCH 02/12] chore: bench index map --- benches/bench.rs | 6 ++++++ benches/benchmark_repetitive_react_components.rs | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/benches/bench.rs b/benches/bench.rs index 661359f8..1694565b 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -29,6 +29,7 @@ use bench_source_map::{ use benchmark_repetitive_react_components::{ benchmark_repetitive_react_components_map, + benchmark_repetitive_react_components_index_map, benchmark_repetitive_react_components_source, }; @@ -188,6 +189,11 @@ fn bench_rspack_sources(criterion: &mut Criterion) { benchmark_repetitive_react_components_map, ); + group.bench_function( + "repetitive_react_components_index_map", + benchmark_repetitive_react_components_index_map, + ); + group.bench_function( "repetitive_react_components_source", benchmark_repetitive_react_components_source, diff --git a/benches/benchmark_repetitive_react_components.rs b/benches/benchmark_repetitive_react_components.rs index 094f107e..6f943cc7 100644 --- a/benches/benchmark_repetitive_react_components.rs +++ b/benches/benchmark_repetitive_react_components.rs @@ -3509,6 +3509,14 @@ pub fn benchmark_repetitive_react_components_map(b: &mut Bencher) { }); } +pub fn benchmark_repetitive_react_components_index_map(b: &mut Bencher) { + let source = REPETITIVE_1K_REACT_COMPONENTS_SOURCE.clone(); + + b.iter(|| { + black_box(source.index_map(&ObjectPool::default(), &MapOptions::default())); + }); +} + pub fn benchmark_repetitive_react_components_source(b: &mut Bencher) { let source = REPETITIVE_1K_REACT_COMPONENTS_SOURCE.clone(); From d9bdb87e31e1b27b470a7db249fa6fcce7d04d5c Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 12:01:13 +0800 Subject: [PATCH 03/12] rename --- benches/bench_complex_replace_source.rs | 4 ++-- src/cached_source.rs | 30 ++++++++++++------------- src/concat_source.rs | 28 +++++++++++------------ src/helpers.rs | 20 ++++++++--------- src/lib.rs | 6 ++--- src/original_source.rs | 24 ++++++++++---------- src/raw_source.rs | 28 +++++++++++------------ src/replace_source.rs | 30 ++++++++++++------------- src/source.rs | 10 ++++----- src/source_map_source.rs | 18 +++++++-------- tests/compat_source.rs | 18 +++++++-------- 11 files changed, 108 insertions(+), 108 deletions(-) diff --git a/benches/bench_complex_replace_source.rs b/benches/bench_complex_replace_source.rs index c3b27157..284f7b7d 100644 --- a/benches/bench_complex_replace_source.rs +++ b/benches/bench_complex_replace_source.rs @@ -9,7 +9,7 @@ pub use criterion::*; pub use codspeed_criterion_compat::*; use rspack_sources::{ - stream_chunks::StreamChunks, BoxSource, CachedSource, MapOptions, ObjectPool, + stream_chunks::ToStream, BoxSource, CachedSource, MapOptions, ObjectPool, OriginalSource, ReplaceSource, Source, SourceExt, }; @@ -36737,7 +36737,7 @@ pub fn benchmark_complex_replace_source_map_cached_source_stream_chunks( cached_source.map(&ObjectPool::default(), &MapOptions::default()); b.iter(|| { - black_box(cached_source.stream_chunks().stream( + black_box(cached_source.to_stream().chunks( &ObjectPool::default(), &MapOptions::default(), &mut |_chunk, _mapping| {}, diff --git a/src/cached_source.rs b/src/cached_source.rs index 1bb5c168..e7853a56 100644 --- a/src/cached_source.rs +++ b/src/cached_source.rs @@ -9,7 +9,7 @@ use rustc_hash::FxHasher; use crate::{ helpers::{ stream_and_get_source_and_map, stream_chunks_of_raw_source, - stream_chunks_of_source_map, Chunks, GeneratedInfo, StreamChunks, + stream_chunks_of_source_map, Stream, GeneratedInfo, ToStream, }, object_pool::ObjectPool, source::{IndexSourceMap, SourceValue}, @@ -182,26 +182,26 @@ impl Source for CachedSource { } } -struct CachedSourceChunks<'source> { - chunks: Box, +struct CachedSourceStream<'source> { + stream: Box, cache: Arc, source: Cow<'source, str>, } -impl<'a> CachedSourceChunks<'a> { +impl<'a> CachedSourceStream<'a> { fn new(cache_source: &'a CachedSource) -> Self { let source = cache_source.source().into_string_lossy(); Self { - chunks: cache_source.inner.stream_chunks(), + stream: cache_source.inner.to_stream(), cache: cache_source.cache.clone(), source, } } } -impl Chunks for CachedSourceChunks<'_> { - fn stream<'a>( +impl Stream for CachedSourceStream<'_> { + fn chunks<'a>( &'a self, object_pool: &'a ObjectPool, options: &MapOptions, @@ -240,7 +240,7 @@ impl Chunks for CachedSourceChunks<'_> { let (generated_info, map) = stream_and_get_source_and_map( options, object_pool, - self.chunks.as_ref(), + self.stream.as_ref(), on_chunk, on_source, on_name, @@ -252,9 +252,9 @@ impl Chunks for CachedSourceChunks<'_> { } } -impl StreamChunks for CachedSource { - fn stream_chunks<'a>(&'a self) -> Box { - Box::new(CachedSourceChunks::new(self)) +impl ToStream for CachedSource { + fn to_stream<'a>(&'a self) -> Box { + Box::new(CachedSourceStream::new(self)) } } @@ -409,8 +409,8 @@ mod tests { let mut on_name_count = 0; let generated_info = { let object_pool = ObjectPool::default(); - let chunks = source.stream_chunks(); - chunks.stream( + let stream = source.to_stream(); + stream.chunks( &object_pool, &map_options, &mut |_chunk, _mapping| { @@ -426,7 +426,7 @@ mod tests { }; let cached_source = CachedSource::new(source); - cached_source.stream_chunks().stream( + cached_source.to_stream().chunks( &ObjectPool::default(), &map_options, &mut |_chunk, _mapping| {}, @@ -437,7 +437,7 @@ mod tests { let mut cached_on_chunk_count = 0; let mut cached_on_source_count = 0; let mut cached_on_name_count = 0; - let cached_generated_info = cached_source.stream_chunks().stream( + let cached_generated_info = cached_source.to_stream().chunks( &ObjectPool::default(), &map_options, &mut |_chunk, _mapping| { diff --git a/src/concat_source.rs b/src/concat_source.rs index 1a7ed84d..e6a2f1f3 100644 --- a/src/concat_source.rs +++ b/src/concat_source.rs @@ -8,7 +8,7 @@ use std::{ use rustc_hash::FxHashMap as HashMap; use crate::{ - helpers::{get_map, Chunks, GeneratedInfo, StreamChunks}, + helpers::{get_map, Stream, GeneratedInfo, ToStream}, linear_map::LinearMap, object_pool::ObjectPool, source::{IndexSourceMap, Mapping, OriginalLocation, Section, SectionOffset}, @@ -211,8 +211,8 @@ impl Source for ConcatSource { object_pool: &'a ObjectPool, options: &MapOptions, ) -> Option { - let chunks = self.stream_chunks(); - let result = get_map(object_pool, chunks.as_ref(), options); + let stream = self.to_stream(); + let result = get_map(object_pool, stream.as_ref(), options); result } @@ -334,23 +334,23 @@ impl PartialEq for ConcatSource { } impl Eq for ConcatSource {} -struct ConcatSourceChunks<'source> { - children_chunks: Vec>, +struct ConcatSourceStream<'source> { + children_chunks: Vec>, } -impl<'source> ConcatSourceChunks<'source> { +impl<'source> ConcatSourceStream<'source> { fn new(concat_source: &'source ConcatSource) -> Self { let children = concat_source.optimized_children(); let children_chunks = children .iter() - .map(|child| child.stream_chunks()) + .map(|child| child.to_stream()) .collect::>(); Self { children_chunks } } } -impl Chunks for ConcatSourceChunks<'_> { - fn stream<'b>( +impl Stream for ConcatSourceStream<'_> { + fn chunks<'b>( &'b self, object_pool: &'b ObjectPool, options: &MapOptions, @@ -359,7 +359,7 @@ impl Chunks for ConcatSourceChunks<'_> { on_name: crate::helpers::OnName<'_, 'b>, ) -> GeneratedInfo { if self.children_chunks.len() == 1 { - return self.children_chunks[0].stream( + return self.children_chunks[0].chunks( object_pool, options, on_chunk, @@ -385,7 +385,7 @@ impl Chunks for ConcatSourceChunks<'_> { let GeneratedInfo { generated_line, generated_column, - } = child_handle.stream( + } = child_handle.chunks( object_pool, options, &mut |chunk, mapping| { @@ -525,9 +525,9 @@ impl Chunks for ConcatSourceChunks<'_> { } } -impl StreamChunks for ConcatSource { - fn stream_chunks<'a>(&'a self) -> Box { - Box::new(ConcatSourceChunks::new(self)) +impl ToStream for ConcatSource { + fn to_stream<'a>(&'a self) -> Box { + Box::new(ConcatSourceStream::new(self)) } } diff --git a/src/helpers.rs b/src/helpers.rs index 4a793358..138f9e2c 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -20,7 +20,7 @@ use crate::{ pub fn get_map<'a>( object_pool: &'a ObjectPool, - chunks: &'a dyn Chunks, + stream: &'a dyn Stream, options: &MapOptions, ) -> Option { let mut mappings_encoder = create_encoder(options.columns); @@ -28,7 +28,7 @@ pub fn get_map<'a>( let mut sources_content: Vec> = Vec::new(); let mut names: Vec = Vec::new(); - chunks.stream( + stream.chunks( object_pool, &MapOptions { columns: options.columns, @@ -72,13 +72,13 @@ pub fn get_map<'a>( /// while building source map information. It's designed to handle the transformation /// of source code into mappings that connect generated code positions to original /// source positions. -pub trait Chunks { +pub trait Stream { /// Streams through source code chunks and generates source map information. /// /// This method processes the source code in chunks, calling the provided callbacks /// for each chunk, source reference, and name reference encountered. It's the core /// method for building source maps during code transformation. - fn stream<'a>( + fn chunks<'a>( &'a self, object_pool: &'a ObjectPool, options: &MapOptions, @@ -88,10 +88,10 @@ pub trait Chunks { ) -> crate::helpers::GeneratedInfo; } -/// [StreamChunks] abstraction, see [webpack-sources source.streamChunks](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/streamChunks.js#L13). -pub trait StreamChunks { - /// [StreamChunks] abstraction - fn stream_chunks<'a>(&'a self) -> Box; +/// [ToStream] abstraction, see [webpack-sources source.streamChunks](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/streamChunks.js#L13). +pub trait ToStream { + /// [ToStream] abstraction + fn to_stream<'a>(&'a self) -> Box; } /// [OnChunk] abstraction, see [webpack-sources onChunk](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/streamChunks.js#L13). @@ -1229,7 +1229,7 @@ pub fn stream_chunks_of_combined_source_map<'a>( pub fn stream_and_get_source_and_map<'a>( options: &MapOptions, object_pool: &'a ObjectPool, - chunks: &'a dyn Chunks, + stream: &'a dyn Stream, on_chunk: OnChunk<'_, 'a>, on_source: OnSource<'_, 'a>, on_name: OnName<'_, 'a>, @@ -1239,7 +1239,7 @@ pub fn stream_and_get_source_and_map<'a>( let mut sources_content: Vec> = Vec::new(); let mut names: Vec = Vec::new(); - let generated_info = chunks.stream( + let generated_info = stream.chunks( object_pool, options, &mut |chunk, mapping| { diff --git a/src/lib.rs b/src/lib.rs index d566b5a3..602858ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,11 +30,11 @@ pub use source_map_source::{ SourceMapSource, SourceMapSourceOptions, WithoutOriginalOptions, }; -/// Reexport `StreamChunks` related types. +/// Reexport `ToStream` related types. pub mod stream_chunks { pub use super::helpers::{ - stream_chunks_default, Chunks, GeneratedInfo, OnChunk, OnName, OnSource, - StreamChunks, + stream_chunks_default, Stream, GeneratedInfo, OnChunk, OnName, OnSource, + ToStream, }; } diff --git a/src/original_source.rs b/src/original_source.rs index e800e7dc..63d22371 100644 --- a/src/original_source.rs +++ b/src/original_source.rs @@ -7,7 +7,7 @@ use std::{ use crate::{ helpers::{ get_generated_source_info, get_map, split_into_lines, - split_into_potential_tokens, Chunks, GeneratedInfo, StreamChunks, + split_into_potential_tokens, Stream, GeneratedInfo, ToStream, }, object_pool::ObjectPool, source::{Mapping, OriginalLocation}, @@ -73,8 +73,8 @@ impl Source for OriginalSource { object_pool: &ObjectPool, options: &MapOptions, ) -> Option { - let chunks = self.stream_chunks(); - get_map(object_pool, chunks.as_ref(), options) + let stream = self.to_stream(); + get_map(object_pool, stream.as_ref(), options) } fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { @@ -111,16 +111,16 @@ impl std::fmt::Debug for OriginalSource { } } -struct OriginalSourceChunks<'a>(&'a OriginalSource); +struct OriginalSourceStream<'a>(&'a OriginalSource); -impl<'source> OriginalSourceChunks<'source> { +impl<'source> OriginalSourceStream<'source> { pub fn new(source: &'source OriginalSource) -> Self { Self(source) } } -impl Chunks for OriginalSourceChunks<'_> { - fn stream<'b>( +impl Stream for OriginalSourceStream<'_> { + fn chunks<'b>( &'b self, _object_pool: &'b ObjectPool, options: &MapOptions, @@ -249,9 +249,9 @@ impl Chunks for OriginalSourceChunks<'_> { } } -impl StreamChunks for OriginalSource { - fn stream_chunks<'a>(&'a self) -> Box { - Box::new(OriginalSourceChunks::new(self)) +impl ToStream for OriginalSource { + fn to_stream<'a>(&'a self) -> Box { + Box::new(OriginalSourceStream::new(self)) } } @@ -365,8 +365,8 @@ mod tests { let source = OriginalSource::new(code, "test.js"); let mut chunks = vec![]; let object_pool = ObjectPool::default(); - let handle = source.stream_chunks(); - let generated_info = handle.stream( + let handle = source.to_stream(); + let generated_info = handle.chunks( &object_pool, &MapOptions::default(), &mut |chunk, mapping| { diff --git a/src/raw_source.rs b/src/raw_source.rs index c7fb29c9..0f0284e6 100644 --- a/src/raw_source.rs +++ b/src/raw_source.rs @@ -6,8 +6,8 @@ use std::{ use crate::{ helpers::{ - get_generated_source_info, stream_chunks_of_raw_source, Chunks, - GeneratedInfo, StreamChunks, + get_generated_source_info, stream_chunks_of_raw_source, Stream, + GeneratedInfo, ToStream, }, object_pool::ObjectPool, MapOptions, Source, SourceMap, SourceValue, @@ -107,15 +107,15 @@ impl Hash for RawStringSource { } } -struct RawStringChunks<'source>(&'source str); +struct RawStringStream<'source>(&'source str); -impl<'source> RawStringChunks<'source> { +impl<'source> RawStringStream<'source> { pub fn new(source: &'source RawStringSource) -> Self { - RawStringChunks(&source.0) + RawStringStream(&source.0) } } -impl Chunks for RawStringChunks<'_> { +impl Stream for RawStringStream<'_> { fn stream<'a>( &'a self, _object_pool: &'a ObjectPool, @@ -132,9 +132,9 @@ impl Chunks for RawStringChunks<'_> { } } -impl StreamChunks for RawStringSource { - fn stream_chunks<'a>(&'a self) -> Box { - Box::new(RawStringChunks::new(self)) +impl ToStream for RawStringSource { + fn to_stream<'a>(&'a self) -> Box { + Box::new(RawStringStream::new(self)) } } @@ -253,9 +253,9 @@ impl Hash for RawBufferSource { } } -struct RawBufferSourceChunks<'a>(&'a RawBufferSource); +struct RawBufferSourceStream<'a>(&'a RawBufferSource); -impl Chunks for RawBufferSourceChunks<'_> { +impl Stream for RawBufferSourceStream<'_> { fn stream<'a>( &'a self, _object_pool: &'a ObjectPool, @@ -273,9 +273,9 @@ impl Chunks for RawBufferSourceChunks<'_> { } } -impl StreamChunks for RawBufferSource { - fn stream_chunks<'a>(&'a self) -> Box { - Box::new(RawBufferSourceChunks(self)) +impl ToStream for RawBufferSource { + fn to_stream<'a>(&'a self) -> Box { + Box::new(RawBufferSourceStream(self)) } } diff --git a/src/replace_source.rs b/src/replace_source.rs index c1a673e3..a5db5dad 100644 --- a/src/replace_source.rs +++ b/src/replace_source.rs @@ -8,7 +8,7 @@ use std::{ use rustc_hash::FxHashMap as HashMap; use crate::{ - helpers::{get_map, split_into_lines, Chunks, GeneratedInfo, StreamChunks}, + helpers::{get_map, split_into_lines, Stream, GeneratedInfo, ToStream}, linear_map::LinearMap, object_pool::ObjectPool, source_content_lines::SourceContentLines, @@ -325,8 +325,8 @@ impl Source for ReplaceSource { if replacements.is_empty() { return self.inner.map(&ObjectPool::default(), options); } - let chunks = self.stream_chunks(); - get_map(&ObjectPool::default(), chunks.as_ref(), options) + let stream = self.to_stream(); + get_map(&ObjectPool::default(), stream.as_ref(), options) } fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { @@ -403,26 +403,26 @@ fn check_content_at_position( } } -struct ReplaceSourceChunks<'a> { +struct ReplaceSourceStream<'a> { is_original_source: bool, - chunks: Box, + stream: Box, replacements: &'a [Replacement], } -impl<'a> ReplaceSourceChunks<'a> { +impl<'a> ReplaceSourceStream<'a> { pub fn new(source: &'a ReplaceSource) -> Self { let is_original_source = source.inner.as_ref().as_any().is::(); Self { is_original_source, - chunks: source.inner.stream_chunks(), + stream: source.inner.to_stream(), replacements: &source.replacements, } } } -impl Chunks for ReplaceSourceChunks<'_> { - fn stream<'a>( +impl Stream for ReplaceSourceStream<'_> { + fn chunks<'a>( &'a self, object_pool: &'a ObjectPool, options: &MapOptions, @@ -501,7 +501,7 @@ impl Chunks for ReplaceSourceChunks<'_> { } }; - let result = self.chunks.stream( + let result = self.stream.chunks( object_pool, &MapOptions { columns: options.columns, @@ -862,9 +862,9 @@ impl Chunks for ReplaceSourceChunks<'_> { } } -impl StreamChunks for ReplaceSource { - fn stream_chunks<'a>(&'a self) -> Box { - Box::new(ReplaceSourceChunks::new(self)) +impl ToStream for ReplaceSource { + fn to_stream<'a>(&'a self) -> Box { + Box::new(ReplaceSourceStream::new(self)) } } @@ -1578,8 +1578,8 @@ return
{data.foo}
let mut chunks = vec![]; let object_pool = ObjectPool::default(); - let handle = source.stream_chunks(); - handle.stream( + let stream = source.to_stream(); + stream.chunks( &object_pool, &MapOptions::default(), &mut |chunk, mapping| { diff --git a/src/source.rs b/src/source.rs index 98f00ac5..1faf6a26 100644 --- a/src/source.rs +++ b/src/source.rs @@ -11,7 +11,7 @@ use dyn_clone::DynClone; use serde::{Deserialize, Serialize}; use crate::{ - helpers::{decode_mappings, encode_mappings, Chunks, StreamChunks}, + helpers::{decode_mappings, encode_mappings, Stream, ToStream}, object_pool::ObjectPool, Result, }; @@ -109,7 +109,7 @@ impl<'a> SourceValue<'a> { /// [Source] abstraction, [webpack-sources docs](https://github.com/webpack/webpack-sources/#source). pub trait Source: - StreamChunks + DynHash + AsAny + DynEq + DynClone + fmt::Debug + Sync + Send + ToStream + DynHash + AsAny + DynEq + DynClone + fmt::Debug + Sync + Send { /// Get the source code. fn source(&self) -> SourceValue<'_>; @@ -205,9 +205,9 @@ impl Source for BoxSource { dyn_clone::clone_trait_object!(Source); -impl StreamChunks for BoxSource { - fn stream_chunks<'a>(&'a self) -> Box { - self.as_ref().stream_chunks() +impl ToStream for BoxSource { + fn to_stream<'a>(&'a self) -> Box { + self.as_ref().to_stream() } } diff --git a/src/source_map_source.rs b/src/source_map_source.rs index 357a26a2..ce9fe6ba 100644 --- a/src/source_map_source.rs +++ b/src/source_map_source.rs @@ -7,7 +7,7 @@ use std::{ use crate::{ helpers::{ get_map, stream_chunks_of_combined_source_map, stream_chunks_of_source_map, - Chunks, StreamChunks, + Stream, ToStream, }, object_pool::ObjectPool, MapOptions, Source, SourceMap, SourceValue, @@ -114,8 +114,8 @@ impl Source for SourceMapSource { if self.inner_source_map.is_none() { return Some(self.source_map.clone()); } - let chunks = self.stream_chunks(); - get_map(object_pool, chunks.as_ref(), options) + let stream = self.to_stream(); + get_map(object_pool, stream.as_ref(), options) } fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { @@ -188,10 +188,10 @@ impl std::fmt::Debug for SourceMapSource { } } -struct SourceMapSourceChunks<'source>(&'source SourceMapSource); +struct SourceMapSourceStream<'source>(&'source SourceMapSource); -impl Chunks for SourceMapSourceChunks<'_> { - fn stream<'a>( +impl Stream for SourceMapSourceStream<'_> { + fn chunks<'a>( &'a self, object_pool: &'a ObjectPool, options: &MapOptions, @@ -227,9 +227,9 @@ impl Chunks for SourceMapSourceChunks<'_> { } } -impl StreamChunks for SourceMapSource { - fn stream_chunks<'a>(&'a self) -> Box { - Box::new(SourceMapSourceChunks(self)) +impl ToStream for SourceMapSource { + fn to_stream<'a>(&'a self) -> Box { + Box::new(SourceMapSourceStream(self)) } } diff --git a/tests/compat_source.rs b/tests/compat_source.rs index 20597052..d5a2530c 100644 --- a/tests/compat_source.rs +++ b/tests/compat_source.rs @@ -3,8 +3,8 @@ use std::borrow::Cow; use std::hash::Hash; use rspack_sources::stream_chunks::{ - stream_chunks_default, Chunks, GeneratedInfo, OnChunk, OnName, OnSource, - StreamChunks, + stream_chunks_default, GeneratedInfo, IntoToStream, OnChunk, OnName, + OnSource, ToStream, }; use rspack_sources::{ ConcatSource, MapOptions, ObjectPool, RawStringSource, Source, SourceExt, @@ -44,15 +44,15 @@ impl Source for CompatSource { } } -struct CompatSourceChunks<'source>(&'static str, Option<&'source SourceMap>); +struct CompatSourceStream<'source>(&'static str, Option<&'source SourceMap>); -impl<'source> CompatSourceChunks<'source> { +impl<'source> CompatSourceStream<'source> { pub fn new(source: &'source CompatSource) -> Self { - CompatSourceChunks(&source.0, source.1.as_ref()) + CompatSourceStream(&source.0, source.1.as_ref()) } } -impl Chunks for CompatSourceChunks<'_> { +impl Stream for CompatSourceStream<'_> { fn stream<'a>( &'a self, object_pool: &'a ObjectPool, @@ -73,9 +73,9 @@ impl Chunks for CompatSourceChunks<'_> { } } -impl StreamChunks for CompatSource { - fn stream_chunks<'a>(&'a self) -> Box { - Box::new(CompatSourceChunks::new(self)) +impl ToStream for CompatSource { + fn stream_chunks<'a>(&'a self) -> Box { + Box::new(CompatSourceStream::new(self)) } } From 5ab3b744ed66c797a73c00dd468a44aefedad7cc Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 14:38:10 +0800 Subject: [PATCH 04/12] on sections --- benches/bench.rs | 2 +- src/cached_source.rs | 11 +++- src/concat_source.rs | 139 +++++++++++++-------------------------- src/helpers.rs | 25 +++++-- src/lib.rs | 8 ++- src/original_source.rs | 24 ++++++- src/raw_source.rs | 33 ++++++++-- src/replace_source.rs | 26 ++++++-- src/source.rs | 62 ++++++----------- src/source_map_source.rs | 24 ++++++- tests/compat_source.rs | 23 +++++-- 11 files changed, 211 insertions(+), 166 deletions(-) diff --git a/benches/bench.rs b/benches/bench.rs index 1694565b..3febb66b 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -28,8 +28,8 @@ use bench_source_map::{ }; use benchmark_repetitive_react_components::{ - benchmark_repetitive_react_components_map, benchmark_repetitive_react_components_index_map, + benchmark_repetitive_react_components_map, benchmark_repetitive_react_components_source, }; diff --git a/src/cached_source.rs b/src/cached_source.rs index e7853a56..01bc74b5 100644 --- a/src/cached_source.rs +++ b/src/cached_source.rs @@ -9,7 +9,7 @@ use rustc_hash::FxHasher; use crate::{ helpers::{ stream_and_get_source_and_map, stream_chunks_of_raw_source, - stream_chunks_of_source_map, Stream, GeneratedInfo, ToStream, + stream_chunks_of_source_map, GeneratedInfo, Stream, ToStream, }, object_pool::ObjectPool, source::{IndexSourceMap, SourceValue}, @@ -250,6 +250,15 @@ impl Stream for CachedSourceStream<'_> { } } } + + fn sections<'a>( + &'a self, + object_pool: &'a ObjectPool, + columns: bool, + on_section: crate::helpers::OnSection<'_, 'a>, + ) -> GeneratedInfo { + todo!() + } } impl ToStream for CachedSource { diff --git a/src/concat_source.rs b/src/concat_source.rs index e6a2f1f3..37c81f82 100644 --- a/src/concat_source.rs +++ b/src/concat_source.rs @@ -2,16 +2,16 @@ use std::{ borrow::Cow, cell::RefCell, hash::{Hash, Hasher}, - sync::{Mutex, OnceLock}, + sync::{Arc, Mutex, OnceLock}, }; use rustc_hash::FxHashMap as HashMap; use crate::{ - helpers::{get_map, Stream, GeneratedInfo, ToStream}, + helpers::{get_map, GeneratedInfo, Stream, ToStream}, linear_map::LinearMap, object_pool::ObjectPool, - source::{IndexSourceMap, Mapping, OriginalLocation, Section, SectionOffset}, + source::{IndexSourceMap, Mapping, OriginalLocation, Section}, BoxSource, MapOptions, RawStringSource, Source, SourceExt, SourceMap, SourceValue, }; @@ -212,8 +212,7 @@ impl Source for ConcatSource { options: &MapOptions, ) -> Option { let stream = self.to_stream(); - let result = get_map(object_pool, stream.as_ref(), options); - result + get_map(object_pool, stream.as_ref(), options).1 } fn index_map( @@ -221,88 +220,16 @@ impl Source for ConcatSource { object_pool: &ObjectPool, options: &MapOptions, ) -> Option { - let children = self.optimized_children(); - - if children.len() == 1 { - return children[0].index_map(object_pool, options); - } - let mut sections = Vec::new(); - let mut current_line_offset: u32 = 0; - let mut current_column_offset: u32 = 0; - - for child in children { - // Get the index map for this child (may itself have sections) - if let Some(child_index_map) = child.index_map(object_pool, options) { - for section in child_index_map.sections() { - // Offset the section by the current position - let line = section.offset.line + current_line_offset; - let column = if section.offset.line == 0 { - section.offset.column + current_column_offset - } else { - section.offset.column - }; - sections.push(Section { - offset: SectionOffset { line, column }, - map: section.map.clone(), - }); - } - } - - // Calculate generated info to determine offset for the next child. - // We iterate via `rope()` to avoid allocating the full source string. - let mut line_count: u32 = 0; - let mut last_line_column: u32 = 0; - let mut ends_with_newline = true; - child.rope(&mut |chunk| { - for byte in chunk.as_bytes() { - if *byte == b'\n' { - line_count += 1; - last_line_column = 0; - ends_with_newline = true; - } else { - ends_with_newline = false; - // Count UTF-16 code units for column offset. - // ASCII bytes (< 0x80): 1 UTF-16 unit (only count leading bytes) - // We only need to count leading bytes of multi-byte sequences. - if (*byte & 0xC0) != 0x80 { - if *byte < 0x80 { - last_line_column += 1; - } else if *byte < 0xF0 { - // 2 or 3 byte sequence -> 1 UTF-16 code unit - last_line_column += 1; - } else { - // 4 byte sequence -> 2 UTF-16 code units (surrogate pair) - last_line_column += 2; - } - } - } + self.to_stream().sections( + object_pool, + options.columns, + &mut |offset, map| { + if let Some(map) = map { + sections.push(Section { offset, map }); } - }); - - if child.size() == 0 { - // Empty child doesn't change the offset - continue; - } - - // Update offsets for next child, matching ConcatSource's concat logic. - // generated_line is like line_count + 1 (1-based), or line_count + 1 if - // ends with newline (extra empty line). - let generated_line = if ends_with_newline { - line_count + 1 - } else { - line_count.max(1) - }; - let generated_column = if ends_with_newline { 0 } else { last_line_column }; - - if generated_line > 1 || line_count > 0 { - current_column_offset = generated_column; - } else { - current_column_offset += generated_column; - } - current_line_offset += line_count; - } - + }, + ); if sections.is_empty() { None } else { @@ -335,17 +262,17 @@ impl PartialEq for ConcatSource { impl Eq for ConcatSource {} struct ConcatSourceStream<'source> { - children_chunks: Vec>, + children_streams: Vec>, } impl<'source> ConcatSourceStream<'source> { fn new(concat_source: &'source ConcatSource) -> Self { let children = concat_source.optimized_children(); - let children_chunks = children + let children_streams = children .iter() .map(|child| child.to_stream()) .collect::>(); - Self { children_chunks } + Self { children_streams } } } @@ -358,8 +285,8 @@ impl Stream for ConcatSourceStream<'_> { on_source: crate::helpers::OnSource<'_, 'b>, on_name: crate::helpers::OnName<'_, 'b>, ) -> GeneratedInfo { - if self.children_chunks.len() == 1 { - return self.children_chunks[0].chunks( + if self.children_streams.len() == 1 { + return self.children_streams[0].chunks( object_pool, options, on_chunk, @@ -378,14 +305,14 @@ impl Stream for ConcatSourceStream<'_> { let name_index_mapping: RefCell> = RefCell::new(LinearMap::default()); - for child_handle in &self.children_chunks { + for child_stream in &self.children_streams { source_index_mapping.borrow_mut().clear(); name_index_mapping.borrow_mut().clear(); let mut last_mapping_line = 0; let GeneratedInfo { generated_line, generated_column, - } = child_handle.chunks( + } = child_stream.chunks( object_pool, options, &mut |chunk, mapping| { @@ -523,6 +450,34 @@ impl Stream for ConcatSourceStream<'_> { generated_column: current_column_offset, } } + + fn sections<'a>( + &'a self, + object_pool: &'a ObjectPool, + columns: bool, + on_section: crate::helpers::OnSection<'_, 'a>, + ) -> GeneratedInfo { + let mut current_generated_info = GeneratedInfo { + generated_line: 1, + generated_column: 0, + }; + + for child_stream in &self.children_streams { + let generated_info = child_stream.sections( + object_pool, + columns, + &mut |mut offset, mapping| { + offset.line += current_generated_info.generated_line - 1; + offset.column += current_generated_info.generated_column; + on_section(offset, mapping); + }, + ); + current_generated_info.generated_line += + generated_info.generated_line - 1; + current_generated_info.generated_column = generated_info.generated_column; + } + current_generated_info + } } impl ToStream for ConcatSource { diff --git a/src/helpers.rs b/src/helpers.rs index 138f9e2c..3ad16d52 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -15,20 +15,20 @@ use crate::{ source::{Mapping, OriginalLocation}, source_content_lines::SourceContentLines, with_utf16::WithUtf16, - MapOptions, SourceMap, + MapOptions, SectionOffset, SourceMap, }; pub fn get_map<'a>( object_pool: &'a ObjectPool, stream: &'a dyn Stream, options: &MapOptions, -) -> Option { +) -> (GeneratedInfo, Option) { let mut mappings_encoder = create_encoder(options.columns); let mut sources: Vec = Vec::new(); let mut sources_content: Vec> = Vec::new(); let mut names: Vec = Vec::new(); - stream.chunks( + let generated_info = stream.chunks( object_pool, &MapOptions { columns: options.columns, @@ -61,9 +61,12 @@ pub fn get_map<'a>( names[name_index] = name.to_string(); }, ); + let mappings = mappings_encoder.drain(); - (!mappings.is_empty()) - .then(|| SourceMap::new(mappings, sources, sources_content, names)) + let map = (!mappings.is_empty()) + .then(|| SourceMap::new(mappings, sources, sources_content, names)); + + (generated_info, map) } /// A trait for processing source code chunks and generating source maps. @@ -86,6 +89,13 @@ pub trait Stream { on_source: crate::helpers::OnSource<'_, 'a>, on_name: crate::helpers::OnName<'_, 'a>, ) -> crate::helpers::GeneratedInfo; + + fn sections<'a>( + &'a self, + object_pool: &'a ObjectPool, + columns: bool, + on_section: crate::helpers::OnSection<'_, 'a>, + ) -> crate::helpers::GeneratedInfo; } /// [ToStream] abstraction, see [webpack-sources source.streamChunks](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/streamChunks.js#L13). @@ -105,6 +115,9 @@ pub type OnSource<'a, 'b> = /// [OnName] abstraction, see [webpack-sources onName](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/streamChunks.js#L13). pub type OnName<'a, 'b> = &'a mut dyn FnMut(u32, Cow<'b, str>); +pub type OnSection<'a, 'b> = + &'a mut dyn FnMut(SectionOffset, Option); + /// Default stream chunks behavior impl, see [webpack-sources streamChunks](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/streamChunks.js#L15-L35). pub fn stream_chunks_default<'a>( options: &MapOptions, @@ -131,7 +144,7 @@ pub fn stream_chunks_default<'a>( } /// `GeneratedSourceInfo` abstraction, see [webpack-sources GeneratedSourceInfo](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/getGeneratedSourceInfo.js) -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct GeneratedInfo { /// Generated line pub generated_line: u32, diff --git a/src/lib.rs b/src/lib.rs index 602858ab..6493876d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,11 +33,13 @@ pub use source_map_source::{ /// Reexport `ToStream` related types. pub mod stream_chunks { pub use super::helpers::{ - stream_chunks_default, Stream, GeneratedInfo, OnChunk, OnName, OnSource, - ToStream, + stream_chunks_default, GeneratedInfo, OnChunk, OnName, OnSection, OnSource, + Stream, ToStream, }; } -pub use helpers::{decode_mappings, encode_mappings}; +pub use helpers::{ + decode_mappings, encode_mappings, get_generated_source_info, +}; pub use object_pool::ObjectPool; diff --git a/src/original_source.rs b/src/original_source.rs index 63d22371..de964c2a 100644 --- a/src/original_source.rs +++ b/src/original_source.rs @@ -7,11 +7,11 @@ use std::{ use crate::{ helpers::{ get_generated_source_info, get_map, split_into_lines, - split_into_potential_tokens, Stream, GeneratedInfo, ToStream, + split_into_potential_tokens, GeneratedInfo, Stream, ToStream, }, object_pool::ObjectPool, source::{Mapping, OriginalLocation}, - MapOptions, Source, SourceMap, SourceValue, + MapOptions, SectionOffset, Source, SourceMap, SourceValue, }; /// Represents source code, it will create source map for the source code, @@ -74,7 +74,7 @@ impl Source for OriginalSource { options: &MapOptions, ) -> Option { let stream = self.to_stream(); - get_map(object_pool, stream.as_ref(), options) + get_map(object_pool, stream.as_ref(), options).1 } fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { @@ -247,6 +247,24 @@ impl Stream for OriginalSourceStream<'_> { } } } + + fn sections<'a>( + &'a self, + object_pool: &'a ObjectPool, + columns: bool, + on_section: crate::helpers::OnSection<'_, 'a>, + ) -> GeneratedInfo { + let (generated_info, map) = get_map( + object_pool, + self, + &MapOptions { + columns, + final_source: true, + }, + ); + on_section(SectionOffset::default(), map); + generated_info + } } impl ToStream for OriginalSource { diff --git a/src/raw_source.rs b/src/raw_source.rs index 0f0284e6..879eb3c3 100644 --- a/src/raw_source.rs +++ b/src/raw_source.rs @@ -6,11 +6,11 @@ use std::{ use crate::{ helpers::{ - get_generated_source_info, stream_chunks_of_raw_source, Stream, - GeneratedInfo, ToStream, + get_generated_source_info, stream_chunks_of_raw_source, GeneratedInfo, + Stream, ToStream, }, object_pool::ObjectPool, - MapOptions, Source, SourceMap, SourceValue, + MapOptions, SectionOffset, Source, SourceMap, SourceValue, }; /// A string variant of [RawStringSource]. @@ -116,7 +116,7 @@ impl<'source> RawStringStream<'source> { } impl Stream for RawStringStream<'_> { - fn stream<'a>( + fn chunks<'a>( &'a self, _object_pool: &'a ObjectPool, options: &MapOptions, @@ -130,6 +130,17 @@ impl Stream for RawStringStream<'_> { stream_chunks_of_raw_source(self.0, options, on_chunk, on_source, on_name) } } + + fn sections<'a>( + &'a self, + _object_pool: &'a ObjectPool, + _columns: bool, + on_section: crate::helpers::OnSection<'_, 'a>, + ) -> GeneratedInfo { + let generated_info = get_generated_source_info(self.0); + on_section(SectionOffset::default(), None); + generated_info + } } impl ToStream for RawStringSource { @@ -256,7 +267,7 @@ impl Hash for RawBufferSource { struct RawBufferSourceStream<'a>(&'a RawBufferSource); impl Stream for RawBufferSourceStream<'_> { - fn stream<'a>( + fn chunks<'a>( &'a self, _object_pool: &'a ObjectPool, options: &MapOptions, @@ -271,6 +282,18 @@ impl Stream for RawBufferSourceStream<'_> { stream_chunks_of_raw_source(code, options, on_chunk, on_source, on_name) } } + + fn sections<'a>( + &'a self, + _object_pool: &'a ObjectPool, + _columns: bool, + on_section: crate::helpers::OnSection<'_, 'a>, + ) -> GeneratedInfo { + let code = self.0.get_or_init_value_as_string(); + let generated_info = get_generated_source_info(code); + on_section(SectionOffset::default(), None); + generated_info + } } impl ToStream for RawBufferSource { diff --git a/src/replace_source.rs b/src/replace_source.rs index a5db5dad..e69923ab 100644 --- a/src/replace_source.rs +++ b/src/replace_source.rs @@ -8,12 +8,12 @@ use std::{ use rustc_hash::FxHashMap as HashMap; use crate::{ - helpers::{get_map, split_into_lines, Stream, GeneratedInfo, ToStream}, + helpers::{get_map, split_into_lines, GeneratedInfo, Stream, ToStream}, linear_map::LinearMap, object_pool::ObjectPool, source_content_lines::SourceContentLines, - BoxSource, MapOptions, Mapping, OriginalLocation, OriginalSource, Source, - SourceExt, SourceMap, SourceValue, + BoxSource, MapOptions, Mapping, OriginalLocation, OriginalSource, + SectionOffset, Source, SourceExt, SourceMap, SourceValue, }; /// Decorates a Source with replacements and insertions of source code, @@ -326,7 +326,7 @@ impl Source for ReplaceSource { return self.inner.map(&ObjectPool::default(), options); } let stream = self.to_stream(); - get_map(&ObjectPool::default(), stream.as_ref(), options) + get_map(&ObjectPool::default(), stream.as_ref(), options).1 } fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { @@ -860,6 +860,24 @@ impl Stream for ReplaceSourceStream<'_> { }) as u32, } } + + fn sections<'a>( + &'a self, + object_pool: &'a ObjectPool, + columns: bool, + on_section: crate::helpers::OnSection<'_, 'a>, + ) -> GeneratedInfo { + let (generated_info, map) = get_map( + object_pool, + self, + &MapOptions { + columns, + final_source: true, + }, + ); + on_section(SectionOffset::default(), map); + generated_info + } } impl ToStream for ReplaceSource { diff --git a/src/source.rs b/src/source.rs index 1faf6a26..166eaee6 100644 --- a/src/source.rs +++ b/src/source.rs @@ -605,7 +605,7 @@ impl TryFrom for SourceMap { /// /// Both `line` and `column` are 0-based, as specified by the /// [Index Source Map](https://tc39.es/ecma426/#sec-index-source-map) format. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Copy, Default)] pub struct SectionOffset { /// 0-based line offset in the generated code. pub line: u32, @@ -783,11 +783,9 @@ impl IndexSourceMap { .unwrap_or(&orig.source_index), original_line: orig.original_line, original_column: orig.original_column, - name_index: orig.name_index.map(|ni| { - *local_name_mapping - .get(ni as usize) - .unwrap_or(&ni) - }), + name_index: orig + .name_index + .map(|ni| *local_name_mapping.get(ni as usize).unwrap_or(&ni)), }); all_mappings.push(Mapping { @@ -803,8 +801,12 @@ impl IndexSourceMap { } let mappings_str = encode_mappings(all_mappings.into_iter()); - let mut result = - SourceMap::new(mappings_str, global_sources, global_sources_content, global_names); + let mut result = SourceMap::new( + mappings_str, + global_sources, + global_sources_content, + global_names, + ); if self.file.is_some() { result.set_file(self.file.clone()); } @@ -1125,34 +1127,20 @@ mod tests { assert_eq!(mappings.len(), 2); assert_eq!(mappings[0].generated_line, 1); assert_eq!(mappings[0].generated_column, 0); - assert_eq!( - mappings[0].original.as_ref().unwrap().source_index, - 0 - ); + assert_eq!(mappings[0].original.as_ref().unwrap().source_index, 0); assert_eq!(mappings[1].generated_line, 2); assert_eq!(mappings[1].generated_column, 0); - assert_eq!( - mappings[1].original.as_ref().unwrap().source_index, - 1 - ); + assert_eq!(mappings[1].original.as_ref().unwrap().source_index, 1); } #[test] fn index_source_map_to_source_map_with_column_offset() { // First section at line 0, col 0 - let map1 = SourceMap::new( - "AAAA", - vec!["a.js".into()], - vec!["hello".into()], - vec![], - ); + let map1 = + SourceMap::new("AAAA", vec!["a.js".into()], vec!["hello".into()], vec![]); // Second section at line 0, col 5 (same line, after "hello") - let map2 = SourceMap::new( - "AAAA", - vec!["b.js".into()], - vec!["world".into()], - vec![], - ); + let map2 = + SourceMap::new("AAAA", vec!["b.js".into()], vec!["world".into()], vec![]); let index_map = IndexSourceMap::new(vec![ Section { offset: SectionOffset { line: 0, column: 0 }, @@ -1196,12 +1184,8 @@ mod tests { #[test] fn index_map_file_field_propagated() { - let map = SourceMap::new( - "AAAA", - vec!["a.js".into()], - vec!["hello".into()], - vec![], - ); + let map = + SourceMap::new("AAAA", vec!["a.js".into()], vec!["hello".into()], vec![]); let mut index_map = IndexSourceMap::new(vec![Section { offset: SectionOffset { line: 0, column: 0 }, map, @@ -1244,13 +1228,7 @@ mod tests { assert_eq!(result.sources()[0], "shared.js"); // Both mappings should reference source index 0 let mappings: Vec = result.decoded_mappings().collect(); - assert_eq!( - mappings[0].original.as_ref().unwrap().source_index, - 0 - ); - assert_eq!( - mappings[1].original.as_ref().unwrap().source_index, - 0 - ); + assert_eq!(mappings[0].original.as_ref().unwrap().source_index, 0); + assert_eq!(mappings[1].original.as_ref().unwrap().source_index, 0); } } diff --git a/src/source_map_source.rs b/src/source_map_source.rs index ce9fe6ba..b0f624a7 100644 --- a/src/source_map_source.rs +++ b/src/source_map_source.rs @@ -7,10 +7,10 @@ use std::{ use crate::{ helpers::{ get_map, stream_chunks_of_combined_source_map, stream_chunks_of_source_map, - Stream, ToStream, + GeneratedInfo, Stream, ToStream, }, object_pool::ObjectPool, - MapOptions, Source, SourceMap, SourceValue, + MapOptions, SectionOffset, Source, SourceMap, SourceValue, }; /// Options for [SourceMapSource::new]. @@ -115,7 +115,7 @@ impl Source for SourceMapSource { return Some(self.source_map.clone()); } let stream = self.to_stream(); - get_map(object_pool, stream.as_ref(), options) + get_map(object_pool, stream.as_ref(), options).1 } fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { @@ -225,6 +225,24 @@ impl Stream for SourceMapSourceStream<'_> { ) } } + + fn sections<'a>( + &'a self, + object_pool: &'a ObjectPool, + columns: bool, + on_section: crate::helpers::OnSection<'_, 'a>, + ) -> GeneratedInfo { + let (generated_info, map) = get_map( + object_pool, + self, + &MapOptions { + columns, + final_source: true, + }, + ); + on_section(SectionOffset::default(), map); + generated_info + } } impl ToStream for SourceMapSource { diff --git a/tests/compat_source.rs b/tests/compat_source.rs index d5a2530c..63bb6a2c 100644 --- a/tests/compat_source.rs +++ b/tests/compat_source.rs @@ -3,12 +3,12 @@ use std::borrow::Cow; use std::hash::Hash; use rspack_sources::stream_chunks::{ - stream_chunks_default, GeneratedInfo, IntoToStream, OnChunk, OnName, - OnSource, ToStream, + stream_chunks_default, GeneratedInfo, OnChunk, OnName, OnSection, OnSource, + Stream, ToStream, }; use rspack_sources::{ - ConcatSource, MapOptions, ObjectPool, RawStringSource, Source, SourceExt, - SourceMap, SourceValue, + get_generated_source_info, ConcatSource, MapOptions, ObjectPool, + RawStringSource, SectionOffset, Source, SourceExt, SourceMap, SourceValue, }; #[derive(Debug, Eq)] @@ -53,7 +53,7 @@ impl<'source> CompatSourceStream<'source> { } impl Stream for CompatSourceStream<'_> { - fn stream<'a>( + fn chunks<'a>( &'a self, object_pool: &'a ObjectPool, options: &MapOptions, @@ -71,10 +71,21 @@ impl Stream for CompatSourceStream<'_> { on_name, ) } + + fn sections<'a>( + &'a self, + _object_pool: &'a ObjectPool, + _columns: bool, + on_section: OnSection<'_, 'a>, + ) -> GeneratedInfo { + let generated_info = get_generated_source_info(self.0); + on_section(SectionOffset::default(), self.1.cloned()); + generated_info + } } impl ToStream for CompatSource { - fn stream_chunks<'a>(&'a self) -> Box { + fn to_stream<'a>(&'a self) -> Box { Box::new(CompatSourceStream::new(self)) } } From 346bbd30a78e18f83fb305f17d66253d4ae38e5e Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 15:32:44 +0800 Subject: [PATCH 05/12] impl sections in cached source --- src/cached_source.rs | 50 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/cached_source.rs b/src/cached_source.rs index 01bc74b5..94e4060a 100644 --- a/src/cached_source.rs +++ b/src/cached_source.rs @@ -8,11 +8,12 @@ use rustc_hash::FxHasher; use crate::{ helpers::{ - stream_and_get_source_and_map, stream_chunks_of_raw_source, - stream_chunks_of_source_map, GeneratedInfo, Stream, ToStream, + get_generated_source_info, stream_and_get_source_and_map, + stream_chunks_of_raw_source, stream_chunks_of_source_map, GeneratedInfo, + Stream, ToStream, }, object_pool::ObjectPool, - source::{IndexSourceMap, SourceValue}, + source::{IndexSourceMap, Section, SectionOffset, SourceValue}, BoxSource, MapOptions, RawBufferSource, Source, SourceExt, SourceMap, }; @@ -257,7 +258,48 @@ impl Stream for CachedSourceStream<'_> { columns: bool, on_section: crate::helpers::OnSection<'_, 'a>, ) -> GeneratedInfo { - todo!() + let cell = if columns { + &self.cache.columns_index_map + } else { + &self.cache.line_only_index_map + }; + match cell.get() { + Some(index_map) => { + let generated_info = + get_generated_source_info(self.source.as_ref()); + if let Some(index_map) = index_map { + for section in index_map.sections() { + on_section(section.offset, Some(section.map.clone())); + } + } else { + on_section(SectionOffset::default(), None); + } + generated_info + } + None => { + let mut sections = Vec::new(); + let generated_info = self.stream.sections( + object_pool, + columns, + &mut |offset, map| { + if let Some(ref map) = map { + sections.push(Section { + offset, + map: map.clone(), + }); + } + on_section(offset, map); + }, + ); + let index_map = if sections.is_empty() { + None + } else { + Some(IndexSourceMap::new(sections)) + }; + cell.get_or_init(|| index_map); + generated_info + } + } } } From ed51f330522e48ea5a53dd5012a4f7cf8332ff97 Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 16:00:59 +0800 Subject: [PATCH 06/12] sections len --- src/cached_source.rs | 45 ++++++++++++++++++++++++++-------------- src/concat_source.rs | 39 +++++++++++++++++++++------------- src/helpers.rs | 10 +++++++++ src/original_source.rs | 5 +++++ src/raw_source.rs | 10 +++++++++ src/replace_source.rs | 5 +++++ src/source.rs | 1 + src/source_map_source.rs | 4 ++++ tests/compat_source.rs | 5 +++++ 9 files changed, 95 insertions(+), 29 deletions(-) diff --git a/src/cached_source.rs b/src/cached_source.rs index 94e4060a..ef914f17 100644 --- a/src/cached_source.rs +++ b/src/cached_source.rs @@ -252,6 +252,22 @@ impl Stream for CachedSourceStream<'_> { } } + fn sections_size_hint(&self) -> usize { + if let Some(index_map) = self.cache.columns_index_map.get() { + index_map + .as_ref() + .map(|index_map| index_map.sections().len()) + .unwrap_or(0) + } else if let Some(index_map) = self.cache.line_only_index_map.get() { + index_map + .as_ref() + .map(|index_map| index_map.sections().len()) + .unwrap_or(0) + } else { + self.stream.sections_size_hint() + } + } + fn sections<'a>( &'a self, object_pool: &'a ObjectPool, @@ -265,8 +281,7 @@ impl Stream for CachedSourceStream<'_> { }; match cell.get() { Some(index_map) => { - let generated_info = - get_generated_source_info(self.source.as_ref()); + let generated_info = get_generated_source_info(self.source.as_ref()); if let Some(index_map) = index_map { for section in index_map.sections() { on_section(section.offset, Some(section.map.clone())); @@ -278,19 +293,18 @@ impl Stream for CachedSourceStream<'_> { } None => { let mut sections = Vec::new(); - let generated_info = self.stream.sections( - object_pool, - columns, - &mut |offset, map| { - if let Some(ref map) = map { - sections.push(Section { - offset, - map: map.clone(), - }); - } - on_section(offset, map); - }, - ); + let generated_info = + self + .stream + .sections(object_pool, columns, &mut |offset, map| { + if let Some(ref map) = map { + sections.push(Section { + offset, + map: map.clone(), + }); + } + on_section(offset, map); + }); let index_map = if sections.is_empty() { None } else { @@ -304,6 +318,7 @@ impl Stream for CachedSourceStream<'_> { } impl ToStream for CachedSource { + #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(CachedSourceStream::new(self)) } diff --git a/src/concat_source.rs b/src/concat_source.rs index 37c81f82..ae4ad4c0 100644 --- a/src/concat_source.rs +++ b/src/concat_source.rs @@ -2,7 +2,7 @@ use std::{ borrow::Cow, cell::RefCell, hash::{Hash, Hasher}, - sync::{Arc, Mutex, OnceLock}, + sync::{Mutex, OnceLock}, }; use rustc_hash::FxHashMap as HashMap; @@ -220,16 +220,13 @@ impl Source for ConcatSource { object_pool: &ObjectPool, options: &MapOptions, ) -> Option { - let mut sections = Vec::new(); - self.to_stream().sections( - object_pool, - options.columns, - &mut |offset, map| { - if let Some(map) = map { - sections.push(Section { offset, map }); - } - }, - ); + let stream = self.to_stream(); + let mut sections = Vec::with_capacity(stream.sections_size_hint()); + stream.sections(object_pool, options.columns, &mut |offset, map| { + if let Some(map) = map { + sections.push(Section { offset, map }); + } + }); if sections.is_empty() { None } else { @@ -266,8 +263,7 @@ struct ConcatSourceStream<'source> { } impl<'source> ConcatSourceStream<'source> { - fn new(concat_source: &'source ConcatSource) -> Self { - let children = concat_source.optimized_children(); + fn new(children: &'source [BoxSource]) -> Self { let children_streams = children .iter() .map(|child| child.to_stream()) @@ -451,6 +447,14 @@ impl Stream for ConcatSourceStream<'_> { } } + fn sections_size_hint(&self) -> usize { + self + .children_streams + .iter() + .map(|child_stream| child_stream.sections_size_hint()) + .sum() + } + fn sections<'a>( &'a self, object_pool: &'a ObjectPool, @@ -481,8 +485,15 @@ impl Stream for ConcatSourceStream<'_> { } impl ToStream for ConcatSource { + #[inline] fn to_stream<'a>(&'a self) -> Box { - Box::new(ConcatSourceStream::new(self)) + let children = self.optimized_children(); + // Fast path: delegate directly to the single child's stream, + // avoiding ConcatSourceStream + Vec + extra Box allocations. + if children.len() == 1 { + return children[0].to_stream(); + } + Box::new(ConcatSourceStream::new(children)) } } diff --git a/src/helpers.rs b/src/helpers.rs index 3ad16d52..0a663a53 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -90,6 +90,11 @@ pub trait Stream { on_name: crate::helpers::OnName<'_, 'a>, ) -> crate::helpers::GeneratedInfo; + /// Returns an estimated upper bound of sections that [`Stream::sections`] will produce. + fn sections_size_hint(&self) -> usize; + + /// Streams source map data as discrete sections, calling `on_section` for + /// each section with its offset and optional [`SourceMap`]. fn sections<'a>( &'a self, object_pool: &'a ObjectPool, @@ -115,6 +120,8 @@ pub type OnSource<'a, 'b> = /// [OnName] abstraction, see [webpack-sources onName](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/streamChunks.js#L13). pub type OnName<'a, 'b> = &'a mut dyn FnMut(u32, Cow<'b, str>); +/// Callback invoked for each section during [`Stream::sections`], receiving the +/// section's [`SectionOffset`] and an optional [`SourceMap`]. pub type OnSection<'a, 'b> = &'a mut dyn FnMut(SectionOffset, Option); @@ -291,6 +298,9 @@ pub fn split_into_lines(source: &str) -> impl Iterator { split(source, b'\n') } +/// Computes the [`GeneratedInfo`] (line and column) for the end position of the given source string. +/// +/// See [webpack-sources getGeneratedSourceInfo](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/getGeneratedSourceInfo.js). pub fn get_generated_source_info(source: &str) -> GeneratedInfo { let (generated_line, generated_column) = if source.ends_with('\n') { (split_into_lines(source).count() + 1, 0) diff --git a/src/original_source.rs b/src/original_source.rs index de964c2a..b42750a6 100644 --- a/src/original_source.rs +++ b/src/original_source.rs @@ -248,6 +248,10 @@ impl Stream for OriginalSourceStream<'_> { } } + fn sections_size_hint(&self) -> usize { + 1 + } + fn sections<'a>( &'a self, object_pool: &'a ObjectPool, @@ -268,6 +272,7 @@ impl Stream for OriginalSourceStream<'_> { } impl ToStream for OriginalSource { + #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(OriginalSourceStream::new(self)) } diff --git a/src/raw_source.rs b/src/raw_source.rs index 879eb3c3..28b8a725 100644 --- a/src/raw_source.rs +++ b/src/raw_source.rs @@ -131,6 +131,10 @@ impl Stream for RawStringStream<'_> { } } + fn sections_size_hint(&self) -> usize { + 0 + } + fn sections<'a>( &'a self, _object_pool: &'a ObjectPool, @@ -144,6 +148,7 @@ impl Stream for RawStringStream<'_> { } impl ToStream for RawStringSource { + #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(RawStringStream::new(self)) } @@ -283,6 +288,10 @@ impl Stream for RawBufferSourceStream<'_> { } } + fn sections_size_hint(&self) -> usize { + 0 + } + fn sections<'a>( &'a self, _object_pool: &'a ObjectPool, @@ -297,6 +306,7 @@ impl Stream for RawBufferSourceStream<'_> { } impl ToStream for RawBufferSource { + #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(RawBufferSourceStream(self)) } diff --git a/src/replace_source.rs b/src/replace_source.rs index e69923ab..cb13607b 100644 --- a/src/replace_source.rs +++ b/src/replace_source.rs @@ -861,6 +861,10 @@ impl Stream for ReplaceSourceStream<'_> { } } + fn sections_size_hint(&self) -> usize { + 1 + } + fn sections<'a>( &'a self, object_pool: &'a ObjectPool, @@ -881,6 +885,7 @@ impl Stream for ReplaceSourceStream<'_> { } impl ToStream for ReplaceSource { + #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(ReplaceSourceStream::new(self)) } diff --git a/src/source.rs b/src/source.rs index 166eaee6..8c0dc425 100644 --- a/src/source.rs +++ b/src/source.rs @@ -206,6 +206,7 @@ impl Source for BoxSource { dyn_clone::clone_trait_object!(Source); impl ToStream for BoxSource { + #[inline] fn to_stream<'a>(&'a self) -> Box { self.as_ref().to_stream() } diff --git a/src/source_map_source.rs b/src/source_map_source.rs index b0f624a7..48dcbbdc 100644 --- a/src/source_map_source.rs +++ b/src/source_map_source.rs @@ -226,6 +226,10 @@ impl Stream for SourceMapSourceStream<'_> { } } + fn sections_size_hint(&self) -> usize { + 1 + } + fn sections<'a>( &'a self, object_pool: &'a ObjectPool, diff --git a/tests/compat_source.rs b/tests/compat_source.rs index 63bb6a2c..ac8a1932 100644 --- a/tests/compat_source.rs +++ b/tests/compat_source.rs @@ -72,6 +72,10 @@ impl Stream for CompatSourceStream<'_> { ) } + fn sections_size_hint(&self) -> usize { + 1 + } + fn sections<'a>( &'a self, _object_pool: &'a ObjectPool, @@ -85,6 +89,7 @@ impl Stream for CompatSourceStream<'_> { } impl ToStream for CompatSource { + #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(CompatSourceStream::new(self)) } From 55b756802136abcfd81a241bdd88d7876f1dade5 Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 17:18:14 +0800 Subject: [PATCH 07/12] perf: has_named_replacements --- src/concat_source.rs | 4 +-- src/helpers.rs | 24 +++++++++--------- src/replace_source.rs | 57 ++++++++++++++++++++++++++----------------- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/concat_source.rs b/src/concat_source.rs index ae4ad4c0..8eed9966 100644 --- a/src/concat_source.rs +++ b/src/concat_source.rs @@ -293,7 +293,7 @@ impl Stream for ConcatSourceStream<'_> { let mut current_line_offset = 0; let mut current_column_offset = 0; let mut source_mapping: HashMap, u32> = HashMap::default(); - let mut name_mapping: HashMap, u32> = HashMap::default(); + let mut name_mapping: HashMap<&str, u32> = HashMap::default(); let mut need_to_close_mapping = false; let source_index_mapping: RefCell> = @@ -411,7 +411,7 @@ impl Stream for ConcatSourceStream<'_> { let mut global_index = name_mapping.get(&name).copied(); if global_index.is_none() { let len = name_mapping.len() as u32; - name_mapping.insert(name.clone(), len); + name_mapping.insert(name, len); on_name(len, name); global_index = Some(len); } diff --git a/src/helpers.rs b/src/helpers.rs index 0a663a53..36708440 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -118,7 +118,7 @@ pub type OnSource<'a, 'b> = &'a mut dyn FnMut(u32, Cow<'b, str>, Option<&'b Arc>); /// [OnName] abstraction, see [webpack-sources onName](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/streamChunks.js#L13). -pub type OnName<'a, 'b> = &'a mut dyn FnMut(u32, Cow<'b, str>); +pub type OnName<'a, 'b> = &'a mut dyn FnMut(u32, &'b str); /// Callback invoked for each section during [`Stream::sections`], receiving the /// section's [`SectionOffset`] and an optional [`SourceMap`]. @@ -436,7 +436,7 @@ fn stream_chunks_of_source_map_final<'a>( ) } for (i, name) in source_map.names().iter().enumerate() { - on_name(i as u32, Cow::Borrowed(name)); + on_name(i as u32, name); } let mut mapping_active_line = 0; let mut on_mapping = |mapping: Mapping| { @@ -499,7 +499,7 @@ fn stream_chunks_of_source_map_full<'a>( ) } for (i, name) in source_map.names().iter().enumerate() { - on_name(i as u32, Cow::Borrowed(name)); + on_name(i as u32, name); } let last_line = &lines[lines.len() - 1].line; let last_new_line = last_line.ends_with('\n'); @@ -780,12 +780,12 @@ pub fn stream_chunks_of_combined_source_map<'a>( let inner_source: RefCell>> = RefCell::new(inner_source); let source_mapping: RefCell, u32>> = RefCell::new(HashMap::default()); - let mut name_mapping: HashMap, u32> = HashMap::default(); + let mut name_mapping: HashMap<&str, u32> = HashMap::default(); let source_index_mapping: RefCell> = RefCell::new(LinearMap::default()); let name_index_mapping: RefCell> = RefCell::new(LinearMap::default()); - let name_index_value_mapping: RefCell>> = + let name_index_value_mapping: RefCell> = RefCell::new(LinearMap::default()); let inner_source_index: RefCell = RefCell::new(-2); let inner_source_index_mapping: RefCell> = @@ -799,7 +799,7 @@ pub fn stream_chunks_of_combined_source_map<'a>( > = RefCell::new(LinearMap::default()); let inner_name_index_mapping: RefCell> = RefCell::new(LinearMap::default()); - let inner_name_index_value_mapping: RefCell>> = + let inner_name_index_value_mapping: RefCell> = RefCell::new(LinearMap::default()); let inner_source_map_line_data: RefCell> = RefCell::new(Vec::new()); @@ -962,8 +962,8 @@ pub fn stream_chunks_of_combined_source_map<'a>( let mut global_index = name_mapping.get(name).copied(); if global_index.is_none() { let len = name_mapping.len() as u32; - name_mapping.insert(name.clone(), len); - on_name(len, name.clone()); + name_mapping.insert(name, len); + on_name(len, name); global_index = Some(len); } final_name_index = global_index.unwrap() as i64; @@ -1019,8 +1019,8 @@ pub fn stream_chunks_of_combined_source_map<'a>( let mut global_index = name_mapping.get(name).copied(); if global_index.is_none() { let len = name_mapping.len() as u32; - name_mapping.insert(name.clone(), len); - on_name(len, name.clone()); + name_mapping.insert(name, len); + on_name(len, name); global_index = Some(len); } final_name_index = global_index.unwrap() as i64; @@ -1119,8 +1119,8 @@ pub fn stream_chunks_of_combined_source_map<'a>( let mut global_index = name_mapping.get(name).copied(); if global_index.is_none() { let len = name_mapping.len() as u32; - name_mapping.borrow_mut().insert(name.clone(), len); - on_name(len, name.clone()); + name_mapping.borrow_mut().insert(name, len); + on_name(len, name); global_index = Some(len); } final_name_index = global_index.unwrap() as i64; diff --git a/src/replace_source.rs b/src/replace_source.rs index cb13607b..d74ee06b 100644 --- a/src/replace_source.rs +++ b/src/replace_source.rs @@ -431,17 +431,20 @@ impl Stream for ReplaceSourceStream<'_> { on_name: crate::helpers::OnName<'_, 'a>, ) -> crate::helpers::GeneratedInfo { let on_name = RefCell::new(on_name); - let repls = &self.replacements; + let replacements = &self.replacements; let mut pos: u32 = 0; let mut i: usize = 0; let mut replacement_end: Option = None; - let mut next_replacement = (i < repls.len()).then(|| repls[i].start); + let mut next_replacement = (i < replacements.len()).then(|| replacements[i].start); let mut generated_line_offset: i64 = 0; let mut generated_column_offset: i64 = 0; let mut generated_column_offset_line = 0; let source_content_lines: RefCell>> = RefCell::new(LinearMap::default()); - let name_mapping: RefCell, u32>> = + + // if has named replacements, we need to map the name to the global name index + let has_named_replacements = replacements.iter().any(|repl| repl.name.is_some()); + let name_mapping: RefCell> = RefCell::new(HashMap::default()); let name_index_mapping: RefCell> = RefCell::new(LinearMap::default()); @@ -585,9 +588,13 @@ impl Stream for ReplaceSourceStream<'_> { source_index: original.source_index, original_line: original.original_line, original_column: original.original_column, - name_index: original.name_index.and_then(|name_index| { - name_index_mapping.borrow().get(&name_index).copied() - }), + name_index: if !has_named_replacements { + original.name_index + } else { + original.name_index.and_then(|name_index| { + name_index_mapping.borrow().get(&name_index).copied() + }) + }, } }), }, @@ -612,7 +619,7 @@ impl Stream for ReplaceSourceStream<'_> { // Insert replacement content split into chunks by lines #[allow(unsafe_code)] // SAFETY: The safety of this operation relies on the fact that the `ReplaceSource` type will not delete the `replacements` during its entire lifetime. - let repl = &repls[i]; + let repl = &replacements[i]; let lines = split_into_lines(repl.content.as_str()).collect::>(); @@ -627,8 +634,8 @@ impl Stream for ReplaceSourceStream<'_> { let mut global_index = name_mapping.get(name.as_str()).copied(); if global_index.is_none() { let len = name_mapping.len() as u32; - name_mapping.insert(Cow::Borrowed(name), len); - on_name.borrow_mut()(len, Cow::Borrowed(name)); + name_mapping.insert(name, len); + on_name.borrow_mut()(len, name); global_index = Some(len); } replacement_name_index = global_index; @@ -683,8 +690,8 @@ impl Stream for ReplaceSourceStream<'_> { // Move to next replacement i += 1; - next_replacement = if i < repls.len() { - Some(repls[i].start) + next_replacement = if i < replacements.len() { + Some(replacements[i].start) } else { None }; @@ -792,24 +799,28 @@ impl Stream for ReplaceSourceStream<'_> { on_source(source_index, source, source_content); }, &mut |name_index, name| { - let mut name_mapping = name_mapping.borrow_mut(); - let mut global_index = name_mapping.get(&name).copied(); - if global_index.is_none() { - let len = name_mapping.len() as u32; - name_mapping.insert(name.clone(), len); - on_name.borrow_mut()(len, name); - global_index = Some(len); + if !has_named_replacements { + on_name.borrow_mut()(name_index, name); + } else { + let mut name_mapping = name_mapping.borrow_mut(); + let mut global_index = name_mapping.get(&name).copied(); + if global_index.is_none() { + let len = name_mapping.len() as u32; + name_mapping.insert(name, len); + on_name.borrow_mut()(len, name); + global_index = Some(len); + } + name_index_mapping + .borrow_mut() + .insert(name_index, global_index.unwrap()); } - name_index_mapping - .borrow_mut() - .insert(name_index, global_index.unwrap()); }, ); // Handle remaining replacements one by one let mut line = result.generated_line as i64 + generated_line_offset; - while i < repls.len() { - let content = &repls[i].content; + while i < replacements.len() { + let content = &replacements[i].content; let lines: Vec<&str> = split_into_lines(content).collect(); for (line_idx, content_line) in lines.iter().enumerate() { From 98264705f33a2b608a4ae2c00d2f4d21948ad1bb Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 17:24:49 +0800 Subject: [PATCH 08/12] perf: is_original_source --- src/replace_source.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/replace_source.rs b/src/replace_source.rs index d74ee06b..cbd7c0ff 100644 --- a/src/replace_source.rs +++ b/src/replace_source.rs @@ -435,7 +435,8 @@ impl Stream for ReplaceSourceStream<'_> { let mut pos: u32 = 0; let mut i: usize = 0; let mut replacement_end: Option = None; - let mut next_replacement = (i < replacements.len()).then(|| replacements[i].start); + let mut next_replacement = + (i < replacements.len()).then(|| replacements[i].start); let mut generated_line_offset: i64 = 0; let mut generated_column_offset: i64 = 0; let mut generated_column_offset_line = 0; @@ -443,7 +444,8 @@ impl Stream for ReplaceSourceStream<'_> { RefCell::new(LinearMap::default()); // if has named replacements, we need to map the name to the global name index - let has_named_replacements = replacements.iter().any(|repl| repl.name.is_some()); + let has_named_replacements = + replacements.iter().any(|repl| repl.name.is_some()); let name_mapping: RefCell> = RefCell::new(HashMap::default()); let name_index_mapping: RefCell> = @@ -792,10 +794,12 @@ impl Stream for ReplaceSourceStream<'_> { pos = end_pos; }, &mut |source_index, source, source_content| { - let mut source_content_lines = source_content_lines.borrow_mut(); - let lines = source_content - .map(|source_content| SourceContent::Raw(source_content.clone())); - source_content_lines.insert(source_index, lines); + if !self.is_original_source { + let mut source_content_lines = source_content_lines.borrow_mut(); + let lines = source_content + .map(|source_content| SourceContent::Raw(source_content.clone())); + source_content_lines.insert(source_index, lines); + } on_source(source_index, source, source_content); }, &mut |name_index, name| { From 4eae42c4430955703c079ee135a50bcc6e691e45 Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 17:31:18 +0800 Subject: [PATCH 09/12] revert named replacement --- src/replace_source.rs | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/replace_source.rs b/src/replace_source.rs index cbd7c0ff..5c00763a 100644 --- a/src/replace_source.rs +++ b/src/replace_source.rs @@ -443,9 +443,6 @@ impl Stream for ReplaceSourceStream<'_> { let source_content_lines: RefCell>> = RefCell::new(LinearMap::default()); - // if has named replacements, we need to map the name to the global name index - let has_named_replacements = - replacements.iter().any(|repl| repl.name.is_some()); let name_mapping: RefCell> = RefCell::new(HashMap::default()); let name_index_mapping: RefCell> = @@ -590,13 +587,9 @@ impl Stream for ReplaceSourceStream<'_> { source_index: original.source_index, original_line: original.original_line, original_column: original.original_column, - name_index: if !has_named_replacements { - original.name_index - } else { - original.name_index.and_then(|name_index| { - name_index_mapping.borrow().get(&name_index).copied() - }) - }, + name_index: original.name_index.and_then(|name_index| { + name_index_mapping.borrow().get(&name_index).copied() + }), } }), }, @@ -803,21 +796,17 @@ impl Stream for ReplaceSourceStream<'_> { on_source(source_index, source, source_content); }, &mut |name_index, name| { - if !has_named_replacements { - on_name.borrow_mut()(name_index, name); - } else { - let mut name_mapping = name_mapping.borrow_mut(); - let mut global_index = name_mapping.get(&name).copied(); - if global_index.is_none() { - let len = name_mapping.len() as u32; - name_mapping.insert(name, len); - on_name.borrow_mut()(len, name); - global_index = Some(len); - } - name_index_mapping - .borrow_mut() - .insert(name_index, global_index.unwrap()); + let mut name_mapping = name_mapping.borrow_mut(); + let mut global_index = name_mapping.get(&name).copied(); + if global_index.is_none() { + let len = name_mapping.len() as u32; + name_mapping.insert(name, len); + on_name.borrow_mut()(len, name); + global_index = Some(len); } + name_index_mapping + .borrow_mut() + .insert(name_index, global_index.unwrap()); }, ); From 784e33166fce022efd998466d4953810fcab5953 Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 18:35:20 +0800 Subject: [PATCH 10/12] remove inline --- src/cached_source.rs | 1 - src/concat_source.rs | 1 - src/original_source.rs | 1 - src/raw_source.rs | 1 - src/replace_source.rs | 1 - src/source.rs | 1 - tests/compat_source.rs | 1 - 7 files changed, 7 deletions(-) diff --git a/src/cached_source.rs b/src/cached_source.rs index ef914f17..45e293ad 100644 --- a/src/cached_source.rs +++ b/src/cached_source.rs @@ -318,7 +318,6 @@ impl Stream for CachedSourceStream<'_> { } impl ToStream for CachedSource { - #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(CachedSourceStream::new(self)) } diff --git a/src/concat_source.rs b/src/concat_source.rs index 8eed9966..49603024 100644 --- a/src/concat_source.rs +++ b/src/concat_source.rs @@ -485,7 +485,6 @@ impl Stream for ConcatSourceStream<'_> { } impl ToStream for ConcatSource { - #[inline] fn to_stream<'a>(&'a self) -> Box { let children = self.optimized_children(); // Fast path: delegate directly to the single child's stream, diff --git a/src/original_source.rs b/src/original_source.rs index b42750a6..dc882f5a 100644 --- a/src/original_source.rs +++ b/src/original_source.rs @@ -272,7 +272,6 @@ impl Stream for OriginalSourceStream<'_> { } impl ToStream for OriginalSource { - #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(OriginalSourceStream::new(self)) } diff --git a/src/raw_source.rs b/src/raw_source.rs index 28b8a725..71afb102 100644 --- a/src/raw_source.rs +++ b/src/raw_source.rs @@ -306,7 +306,6 @@ impl Stream for RawBufferSourceStream<'_> { } impl ToStream for RawBufferSource { - #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(RawBufferSourceStream(self)) } diff --git a/src/replace_source.rs b/src/replace_source.rs index 5c00763a..94cab4aa 100644 --- a/src/replace_source.rs +++ b/src/replace_source.rs @@ -889,7 +889,6 @@ impl Stream for ReplaceSourceStream<'_> { } impl ToStream for ReplaceSource { - #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(ReplaceSourceStream::new(self)) } diff --git a/src/source.rs b/src/source.rs index 8c0dc425..166eaee6 100644 --- a/src/source.rs +++ b/src/source.rs @@ -206,7 +206,6 @@ impl Source for BoxSource { dyn_clone::clone_trait_object!(Source); impl ToStream for BoxSource { - #[inline] fn to_stream<'a>(&'a self) -> Box { self.as_ref().to_stream() } diff --git a/tests/compat_source.rs b/tests/compat_source.rs index ac8a1932..13e8b454 100644 --- a/tests/compat_source.rs +++ b/tests/compat_source.rs @@ -89,7 +89,6 @@ impl Stream for CompatSourceStream<'_> { } impl ToStream for CompatSource { - #[inline] fn to_stream<'a>(&'a self) -> Box { Box::new(CompatSourceStream::new(self)) } From 6a3e298871008d94ed4d776cdca72714646d2ea1 Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Fri, 13 Feb 2026 18:41:23 +0800 Subject: [PATCH 11/12] temp --- benches/benchmark_repetitive_react_components.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/benchmark_repetitive_react_components.rs b/benches/benchmark_repetitive_react_components.rs index 6f943cc7..dae047af 100644 --- a/benches/benchmark_repetitive_react_components.rs +++ b/benches/benchmark_repetitive_react_components.rs @@ -3513,7 +3513,7 @@ pub fn benchmark_repetitive_react_components_index_map(b: &mut Bencher) { let source = REPETITIVE_1K_REACT_COMPONENTS_SOURCE.clone(); b.iter(|| { - black_box(source.index_map(&ObjectPool::default(), &MapOptions::default())); + black_box(source.to_stream().sections_size_hint()); }); } From ba214a196ae3f0fec0881b1e966af28f19fbae33 Mon Sep 17 00:00:00 2001 From: Cong-Cong Date: Sun, 15 Feb 2026 17:40:27 +0800 Subject: [PATCH 12/12] perf: optimize and get_generated_source_info --- .../benchmark_repetitive_react_components.rs | 2 +- src/concat_source.rs | 3 +- src/helpers.rs | 31 ++++++++++++------- src/source.rs | 5 ++- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/benches/benchmark_repetitive_react_components.rs b/benches/benchmark_repetitive_react_components.rs index dae047af..6f943cc7 100644 --- a/benches/benchmark_repetitive_react_components.rs +++ b/benches/benchmark_repetitive_react_components.rs @@ -3513,7 +3513,7 @@ pub fn benchmark_repetitive_react_components_index_map(b: &mut Bencher) { let source = REPETITIVE_1K_REACT_COMPONENTS_SOURCE.clone(); b.iter(|| { - black_box(source.to_stream().sections_size_hint()); + black_box(source.index_map(&ObjectPool::default(), &MapOptions::default())); }); } diff --git a/src/concat_source.rs b/src/concat_source.rs index 49603024..ff18babe 100644 --- a/src/concat_source.rs +++ b/src/concat_source.rs @@ -523,6 +523,7 @@ fn optimize(children: &mut Vec) -> Vec { } /// Helper function to merge and flush pending raw sources. +#[inline(always)] fn merge_raw_sources( raw_sources: &mut Vec, new_children: &mut Vec, @@ -538,7 +539,7 @@ fn merge_raw_sources( let capacity = raw_sources.iter().map(|s| s.size()).sum(); let mut merged_content = String::with_capacity(capacity); for source in raw_sources.drain(..) { - merged_content.push_str(source.source().into_string_lossy().as_ref()); + source.rope(&mut |chunk| merged_content.push_str(chunk)); } let merged_source = RawStringSource::from(merged_content); new_children.push(merged_source.boxed()); diff --git a/src/helpers.rs b/src/helpers.rs index 36708440..0a40e28c 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -302,21 +302,30 @@ pub fn split_into_lines(source: &str) -> impl Iterator { /// /// See [webpack-sources getGeneratedSourceInfo](https://github.com/webpack/webpack-sources/blob/9f98066311d53a153fdc7c633422a1d086528027/lib/helpers/getGeneratedSourceInfo.js). pub fn get_generated_source_info(source: &str) -> GeneratedInfo { - let (generated_line, generated_column) = if source.ends_with('\n') { - (split_into_lines(source).count() + 1, 0) - } else { - let mut line_count = 0; - let mut last_line = ""; + let bytes = source.as_bytes(); - for line in split_into_lines(source) { - line_count += 1; - last_line = line; - } + let mut line_count = 0; + let mut last_newline_pos = None; + + for pos in memchr::memchr_iter(b'\n', bytes) { + line_count += 1; + last_newline_pos = Some(pos); + } - (line_count.max(1), last_line.encode_utf16().count()) + let generated_column = if let Some(pos) = last_newline_pos { + if pos == bytes.len() - 1 { + 0 + } else { + #[allow(unsafe_code)] + let last_line_slice = unsafe { source.get_unchecked(pos + 1..) }; + last_line_slice.chars().map(|c| c.len_utf16()).sum() + } + } else { + source.chars().map(|c| c.len_utf16()).sum() }; + GeneratedInfo { - generated_line: generated_line as u32, + generated_line: line_count + 1, generated_column: generated_column as u32, } } diff --git a/src/source.rs b/src/source.rs index 166eaee6..627a65c0 100644 --- a/src/source.rs +++ b/src/source.rs @@ -739,9 +739,8 @@ impl IndexSourceMap { let idx = global_sources.len() as u32; source_mapping.insert(source.clone(), idx); global_sources.push(source.clone()); - while global_sources_content.len() < global_sources.len() { - global_sources_content.push("".into()); - } + global_sources_content + .resize_with(global_sources.len(), || "".into()); if let Some(content) = map.get_source_content(i) { global_sources_content[idx as usize] = content.clone(); }