diff --git a/Cargo.lock b/Cargo.lock index 9a37f97d..37473f64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,6 +559,7 @@ dependencies = [ "memchr", "regex", "rustc-hash", + "self_cell", "serde", "serde_json", "simd-json", @@ -606,6 +607,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "self_cell" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" + [[package]] name = "serde" version = "1.0.216" diff --git a/Cargo.toml b/Cargo.toml index ca094b23..76dfbeba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ itertools = "0.13" codspeed-criterion-compat = { version = "2.7.2", default-features = false, optional = true } static_assertions = "1.1.0" simd-json = "0.14.3" +self_cell = "1.2.0" [dev-dependencies] twox-hash = "2.1.0" diff --git a/benches/bench.rs b/benches/bench.rs index 81736b40..2d3b1f80 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -18,8 +18,7 @@ use rspack_sources::{ use bench_complex_replace_source::benchmark_complex_replace_source; use bench_source_map::{ - benchmark_parse_source_map_from_json, benchmark_source_map_clone, - benchmark_stringify_source_map_to_json, + benchmark_parse_source_map_from_json, benchmark_stringify_source_map_to_json, }; const HELLOWORLD_JS: &str = include_str!(concat!( @@ -253,8 +252,6 @@ fn bench_rspack_sources(criterion: &mut Criterion) { benchmark_parse_source_map_from_json, ); - group.bench_function("source_map_clone", benchmark_source_map_clone); - group.bench_function( "stringify_source_map_to_json", benchmark_stringify_source_map_to_json, diff --git a/benches/bench_source_map.rs b/benches/bench_source_map.rs index 879c6a84..94ab5178 100644 --- a/benches/bench_source_map.rs +++ b/benches/bench_source_map.rs @@ -19,13 +19,6 @@ pub fn benchmark_parse_source_map_from_json(b: &mut Bencher) { }) } -pub fn benchmark_source_map_clone(b: &mut Bencher) { - let source = SourceMap::from_json(ANTD_MIN_JS_MAP).unwrap(); - b.iter(|| { - let _ = black_box(source.clone()); - }) -} - pub fn benchmark_stringify_source_map_to_json(b: &mut Bencher) { let source = SourceMap::from_json(ANTD_MIN_JS_MAP).unwrap(); b.iter(|| { diff --git a/src/error.rs b/src/error.rs index 6ff7e2c9..94d12001 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,6 +10,8 @@ pub enum Error { BadJson(simd_json::Error), /// rope related failure Rope(&'static str), + /// IO related failure + IO(std::io::Error), } impl fmt::Display for Error { @@ -17,6 +19,7 @@ impl fmt::Display for Error { match self { Error::BadJson(err) => write!(f, "bad json: {err}"), Error::Rope(err) => write!(f, "rope error: {err}"), + Error::IO(err) => write!(f, "io error: {err}"), } } } @@ -28,3 +31,9 @@ impl From for Error { Error::BadJson(err) } } + +impl From for Error { + fn from(err: std::io::Error) -> Error { + Error::IO(err) + } +} diff --git a/src/helpers.rs b/src/helpers.rs index a096f708..3c2dd60b 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -20,9 +20,9 @@ use crate::{ type InnerSourceContentLine<'a, 'b> = RefCell>>>>>>; -pub fn get_map<'a, S: StreamChunks>( - stream: &'a S, - options: &'a MapOptions, +pub fn get_map( + stream: &S, + options: &MapOptions, ) -> Option { let mut mappings_encoder = create_encoder(options.columns); let mut sources: Vec = Vec::new(); @@ -367,14 +367,14 @@ where if result.generated_line == 1 && result.generated_column == 0 { return result; } - for (i, source) in source_map.sources().iter().enumerate() { + for (i, source) in source_map.sources().enumerate() { on_source( i as u32, get_source(source_map, source), source_map.get_source_content(i).map(Rope::from), ) } - for (i, name) in source_map.names().iter().enumerate() { + for (i, name) in source_map.names().enumerate() { on_name(i as u32, Cow::Borrowed(name)); } let mut mapping_active_line = 0; @@ -431,14 +431,14 @@ where generated_column: 0, }; } - for (i, source) in source_map.sources().iter().enumerate() { + for (i, source) in source_map.sources().enumerate() { on_source( i as u32, get_source(source_map, source), source_map.get_source_content(i).map(Rope::from), ) } - for (i, name) in source_map.names().iter().enumerate() { + for (i, name) in source_map.names().enumerate() { on_name(i as u32, Cow::Borrowed(name)); } let last_line = @@ -581,7 +581,7 @@ where generated_column: 0, }; } - for (i, source) in source_map.sources().iter().enumerate() { + for (i, source) in source_map.sources().enumerate() { on_source( i as u32, get_source(source_map, source), @@ -629,7 +629,7 @@ where generated_column: 0, }; } - for (i, source) in source_map.sources().iter().enumerate() { + for (i, source) in source_map.sources().enumerate() { on_source( i as u32, get_source(source_map, source), diff --git a/src/lib.rs b/src/lib.rs index 7f16b8fa..ee88cf91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ mod raw_source; mod replace_source; mod rope; mod source; +mod source_map; mod source_map_source; mod with_indices; @@ -24,8 +25,8 @@ pub use replace_source::{ReplaceSource, ReplacementEnforce}; pub use rope::Rope; pub use source::{ BoxSource, MapOptions, Mapping, OriginalLocation, Source, SourceExt, - SourceMap, }; +pub use source_map::SourceMap; pub use source_map_source::{ SourceMapSource, SourceMapSourceOptions, WithoutOriginalOptions, }; diff --git a/src/original_source.rs b/src/original_source.rs index 0bac5ab5..b2588d60 100644 --- a/src/original_source.rs +++ b/src/original_source.rs @@ -248,14 +248,20 @@ mod tests { let result_list_map = source.map(&MapOptions::new(false)).unwrap(); assert_eq!(result_text, "Line1\n\nLine3\n"); - assert_eq!(result_map.sources(), &["file.js".to_string()]); - assert_eq!(result_list_map.sources(), ["file.js".to_string()]); assert_eq!( - result_map.sources_content(), + result_map.sources().collect::>(), + ["file.js".to_string()] + ); + assert_eq!( + result_list_map.sources().collect::>(), + ["file.js".to_string()] + ); + assert_eq!( + result_map.sources_content().collect::>(), ["Line1\n\nLine3\n".to_string()], ); assert_eq!( - result_list_map.sources_content(), + result_list_map.sources_content().collect::>(), ["Line1\n\nLine3\n".to_string()], ); assert_eq!(result_map.mappings(), "AAAA;;AAEA"); diff --git a/src/replace_source.rs b/src/replace_source.rs index ff9b87a3..943a6a64 100644 --- a/src/replace_source.rs +++ b/src/replace_source.rs @@ -265,7 +265,7 @@ impl Source for ReplaceSource { self.source().len() } - fn map(&self, options: &crate::MapOptions) -> Option { + fn map(&self, options: &MapOptions) -> Option { let replacements = &self.replacements; if replacements.is_empty() { return self.inner.map(options); @@ -1043,7 +1043,7 @@ Line 2"# #[test] fn should_allow_replacements_at_the_start() { - let map = SourceMap::from_slice( + let map = SourceMap::from_json( r#"{ "version":3, "sources":["abc"], @@ -1051,7 +1051,7 @@ Line 2"# "mappings":";;AAAA,eAAe,SAASA,UAAT,OAA8B;AAAA,MAARC,IAAQ,QAARA,IAAQ;AAC3C,sBAAO;AAAA,cAAMA,IAAI,CAACC;AAAX,IAAP;AACD", "sourcesContent":["export default function StaticPage({ data }) {\nreturn
{data.foo}
\n}\n"], "file":"x" - }"#.as_bytes(), + }"#.to_string(), ).unwrap(); let code = r#"import { jsx as _jsx } from "react/jsx-runtime"; diff --git a/src/source.rs b/src/source.rs index 95ae3b29..4fa5b453 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,27 +1,19 @@ use std::{ any::{Any, TypeId}, borrow::Cow, - convert::{TryFrom, TryInto}, - fmt, + fmt::{self, Debug}, hash::{Hash, Hasher}, sync::Arc, }; -use dyn_clone::DynClone; -use serde::{Deserialize, Serialize}; - -use crate::{ - helpers::{decode_mappings, StreamChunks}, - rope::Rope, - Result, -}; +use crate::{helpers::StreamChunks, rope::Rope, SourceMap}; /// An alias for `Box`. pub type BoxSource = Arc; /// [Source] abstraction, [webpack-sources docs](https://github.com/webpack/webpack-sources/#source). pub trait Source: - StreamChunks + DynHash + AsAny + DynEq + DynClone + fmt::Debug + Sync + Send + StreamChunks + DynHash + AsAny + DynEq + fmt::Debug + Sync + Send { /// Get the source code. fn source(&self) -> Cow; @@ -73,8 +65,6 @@ impl Source for BoxSource { } } -dyn_clone::clone_trait_object!(Source); - impl StreamChunks for BoxSource { fn stream_chunks<'a>( &'a self, @@ -186,295 +176,6 @@ impl MapOptions { } } -fn is_all_empty(val: &Arc<[String]>) -> bool { - if val.is_empty() { - return true; - } - val.iter().all(|s| s.is_empty()) -} - -/// The source map created by [Source::map]. -#[derive(Clone, PartialEq, Eq, Serialize)] -pub struct SourceMap { - version: u8, - #[serde(skip_serializing_if = "Option::is_none")] - file: Option>, - sources: Arc<[String]>, - #[serde(rename = "sourcesContent", skip_serializing_if = "is_all_empty")] - sources_content: Arc<[String]>, - names: Arc<[String]>, - mappings: Arc, - #[serde(rename = "sourceRoot", skip_serializing_if = "Option::is_none")] - source_root: Option>, - #[serde(rename = "debugId", skip_serializing_if = "Option::is_none")] - debug_id: Option>, - #[serde(rename = "ignoreList", skip_serializing_if = "Option::is_none")] - ignore_list: Option>, -} - -impl std::fmt::Debug for SourceMap { - fn fmt( - &self, - f: &mut std::fmt::Formatter<'_>, - ) -> std::result::Result<(), std::fmt::Error> { - let indent = f.width().unwrap_or(0); - let indent_str = format!("{:indent$}", "", indent = indent); - - write!( - f, - "{indent_str}SourceMap::from_json({:?}).unwrap()", - self.clone().to_json().unwrap() - )?; - - Ok(()) - } -} -impl Hash for SourceMap { - fn hash(&self, state: &mut H) { - self.file.hash(state); - self.mappings.hash(state); - self.sources.hash(state); - self.sources_content.hash(state); - self.names.hash(state); - self.source_root.hash(state); - self.ignore_list.hash(state); - } -} - -impl SourceMap { - /// Create a [SourceMap]. - pub fn new( - mappings: Mappings, - sources: Sources, - sources_content: SourcesContent, - names: Names, - ) -> Self - where - Mappings: Into>, - Sources: Into>, - SourcesContent: Into>, - Names: Into>, - { - Self { - version: 3, - file: None, - mappings: mappings.into(), - sources: sources.into(), - sources_content: sources_content.into(), - names: names.into(), - source_root: None, - debug_id: None, - ignore_list: None, - } - } - - /// Get the file field in [SourceMap]. - pub fn file(&self) -> Option<&str> { - self.file.as_deref() - } - - /// Set the file field in [SourceMap]. - pub fn set_file>>(&mut self, file: Option) { - self.file = file.map(Into::into); - } - - /// Get the ignoreList field in [SourceMap]. - pub fn ignore_list(&self) -> Option<&[u32]> { - self.ignore_list.as_deref() - } - - /// Set the ignoreList field in [SourceMap]. - pub fn set_ignore_list>>(&mut self, ignore_list: Option) { - self.ignore_list = ignore_list.map(Into::into); - } - - /// Get the decoded mappings in [SourceMap]. - pub fn decoded_mappings(&self) -> impl Iterator + '_ { - decode_mappings(self) - } - - /// Get the mappings string in [SourceMap]. - pub fn mappings(&self) -> &str { - &self.mappings - } - - /// Get the sources field in [SourceMap]. - pub fn sources(&self) -> &[String] { - &self.sources - } - - /// Set the sources field in [SourceMap]. - pub fn set_sources>>(&mut self, sources: T) { - self.sources = sources.into(); - } - - /// Get the source by index from sources field in [SourceMap]. - pub fn get_source(&self, index: usize) -> Option<&str> { - self.sources.get(index).map(|s| s.as_ref()) - } - - /// Get the sourcesContent field in [SourceMap]. - pub fn sources_content(&self) -> &[String] { - &self.sources_content - } - - /// Set the sourcesContent field in [SourceMap]. - pub fn set_sources_content>>( - &mut self, - sources_content: T, - ) { - self.sources_content = sources_content.into(); - } - - /// Get the source content by index from sourcesContent field in [SourceMap]. - pub fn get_source_content(&self, index: usize) -> Option<&str> { - self.sources_content.get(index).map(|s| s.as_ref()) - } - - /// Get the names field in [SourceMap]. - pub fn names(&self) -> &[String] { - &self.names - } - - /// Set the names field in [SourceMap]. - pub fn set_names>>(&mut self, names: T) { - self.names = names.into(); - } - - /// Get the name by index from names field in [SourceMap]. - pub fn get_name(&self, index: usize) -> Option<&str> { - self.names.get(index).map(|s| s.as_ref()) - } - - /// Get the source_root field in [SourceMap]. - pub fn source_root(&self) -> Option<&str> { - self.source_root.as_deref() - } - - /// Set the source_root field in [SourceMap]. - pub fn set_source_root>>(&mut self, source_root: Option) { - self.source_root = source_root.map(Into::into); - } - - /// Set the debug_id field in [SourceMap]. - pub fn set_debug_id>>(&mut self, debug_id: Option) { - self.debug_id = debug_id.map(Into::into); - } - - /// Get the debug_id field in [SourceMap]. - pub fn get_debug_id(&self) -> Option<&str> { - self.debug_id.as_deref() - } -} - -#[derive(Debug, Default, Deserialize)] -struct RawSourceMap { - pub file: Option, - pub sources: Option>>, - #[serde(rename = "sourceRoot")] - pub source_root: Option, - #[serde(rename = "sourcesContent")] - pub sources_content: Option>>, - pub names: Option>>, - pub mappings: String, - #[serde(rename = "debugId")] - pub debug_id: Option, - #[serde(rename = "ignoreList")] - pub ignore_list: Option>, -} - -impl RawSourceMap { - pub fn from_reader(r: R) -> Result { - let raw: RawSourceMap = simd_json::serde::from_reader(r)?; - Ok(raw) - } - - pub fn from_slice(val: &[u8]) -> Result { - let mut v = val.to_vec(); - let raw: RawSourceMap = simd_json::serde::from_slice(&mut v)?; - Ok(raw) - } - - pub fn from_json(val: &str) -> Result { - let mut v = val.as_bytes().to_vec(); - let raw: RawSourceMap = simd_json::serde::from_slice(&mut v)?; - Ok(raw) - } -} - -impl SourceMap { - /// Create a [SourceMap] from json string. - pub fn from_json(s: &str) -> Result { - RawSourceMap::from_json(s)?.try_into() - } - - /// Create a [SourceMap] from [&[u8]]. - pub fn from_slice(s: &[u8]) -> Result { - RawSourceMap::from_slice(s)?.try_into() - } - - /// Create a [SourceMap] from reader. - pub fn from_reader(s: R) -> Result { - RawSourceMap::from_reader(s)?.try_into() - } - - /// Generate source map to a json string. - pub fn to_json(&self) -> Result { - let json = simd_json::serde::to_string(&self)?; - Ok(json) - } - - /// Generate source map to writer. - pub fn to_writer(self, w: W) -> Result<()> { - simd_json::serde::to_writer(w, &self)?; - Ok(()) - } -} - -impl TryFrom for SourceMap { - type Error = crate::Error; - - fn try_from(raw: RawSourceMap) -> Result { - let file = raw.file.map(Into::into); - let mappings = raw.mappings.into(); - let sources = raw - .sources - .unwrap_or_default() - .into_iter() - .map(Option::unwrap_or_default) - .collect::>() - .into(); - let sources_content = raw - .sources_content - .unwrap_or_default() - .into_iter() - .map(Option::unwrap_or_default) - .collect::>() - .into(); - let names = raw - .names - .unwrap_or_default() - .into_iter() - .map(Option::unwrap_or_default) - .collect::>() - .into(); - let source_root = raw.source_root.map(Into::into); - let debug_id = raw.debug_id.map(Into::into); - - Ok(Self { - version: 3, - file, - mappings, - sources, - sources_content, - names, - source_root, - debug_id, - ignore_list: raw.ignore_list, - }) - } -} - /// Represent a [Mapping] information of source map. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Mapping { @@ -576,7 +277,7 @@ mod tests { RawBufferSource::from("a".as_bytes()).hash(&mut state); (&RawSource::from("h") as &dyn Source).hash(&mut state); ReplaceSource::new(RawSource::from("i").boxed()).hash(&mut state); - assert_eq!(format!("{:x}", state.finish()), "6abed98aa11f84e5"); + assert_eq!(format!("{:x}", state.finish()), "90b46a65420d1a02"); } #[test] @@ -641,12 +342,13 @@ mod tests { value: "c", name: "", source_map: SourceMap::from_json("{\"mappings\": \";\"}").unwrap(), - }); - assert_eq!(c, c.clone()); + }) + .boxed(); + assert_eq!(&c, &c.clone()); let d = ConcatSource::new([RawSource::from("d")]); assert_eq!(d, d.clone()); - let e = CachedSource::new(RawSource::from("e")); - assert_eq!(e, e.clone()); + let e = CachedSource::new(RawSource::from("e")).boxed(); + assert_eq!(&e, &e.clone()); let f = ReplaceSource::new(RawSource::from("f")); assert_eq!(f, f.clone()); let g = RawSource::from("g").boxed(); @@ -655,8 +357,8 @@ mod tests { assert_eq!(h, h); let i = ReplaceSource::new(RawSource::from("i").boxed()); assert_eq!(i, i.clone()); - let j = CachedSource::new(RawSource::from("j").boxed()); - assert_eq!(j, j.clone()); + let j = CachedSource::new(RawSource::from("j").boxed()).boxed(); + assert_eq!(&j, &j.clone()); let k = RawStringSource::from_static("k"); assert_eq!(k, k.clone()); let l = RawBufferSource::from("l".as_bytes()); diff --git a/src/source_map.rs b/src/source_map.rs new file mode 100644 index 00000000..1bdbb326 --- /dev/null +++ b/src/source_map.rs @@ -0,0 +1,561 @@ +use std::{ + hash::{Hash, Hasher}, + sync::Arc, +}; + +use serde::Serialize; +use simd_json::{ + base::ValueAsScalar, + derived::{ValueObjectAccessAsArray, ValueObjectAccessAsScalar}, + BorrowedValue, +}; + +use crate::{helpers::decode_mappings, Mapping, Result}; + +fn is_all_owned_empty(val: &Arc<[String]>) -> bool { + if val.is_empty() { + return true; + } + val.iter().all(|s| s.is_empty()) +} + +/// The source map created by [Source::map]. +#[derive(Clone, PartialEq, Eq, Serialize, Hash)] +pub struct OwnedSourceMap { + version: u8, + #[serde(skip_serializing_if = "Option::is_none")] + file: Option>, + sources: Arc<[String]>, + #[serde( + rename = "sourcesContent", + skip_serializing_if = "is_all_owned_empty" + )] + sources_content: Arc<[String]>, + names: Arc<[String]>, + mappings: Arc, + #[serde(rename = "sourceRoot", skip_serializing_if = "Option::is_none")] + source_root: Option>, + #[serde(rename = "debugId", skip_serializing_if = "Option::is_none")] + debug_id: Option>, + #[serde(rename = "ignoreList", skip_serializing_if = "Option::is_none")] + ignore_list: Option>>, +} + +impl OwnedSourceMap { + pub fn to_json(&self) -> Result { + let json = simd_json::serde::to_string(&self)?; + Ok(json) + } + + pub fn to_writer(&self, w: W) -> Result<()> { + simd_json::serde::to_writer(w, self)?; + Ok(()) + } +} + +fn is_all_borrowed_empty(val: &[&str]) -> bool { + if val.is_empty() { + return true; + } + val.iter().all(|s| s.is_empty()) +} + +#[derive(Clone, PartialEq, Eq, Serialize, Hash)] +struct BorrowedSourceMap<'a> { + version: u8, + #[serde(skip_serializing_if = "Option::is_none")] + file: Option<&'a str>, + sources: Vec<&'a str>, + #[serde( + rename = "sourcesContent", + skip_serializing_if = "is_all_borrowed_empty" + )] + sources_content: Vec<&'a str>, + names: Vec<&'a str>, + mappings: &'a str, + #[serde(rename = "sourceRoot", skip_serializing_if = "Option::is_none")] + source_root: Option<&'a str>, + #[serde(rename = "debugId", skip_serializing_if = "Option::is_none")] + debug_id: Option<&'a str>, + #[serde(rename = "ignoreList", skip_serializing_if = "Option::is_none")] + ignore_list: Option>>, +} + +impl PartialEq for BorrowedSourceMap<'_> { + fn eq(&self, other: &OwnedSourceMap) -> bool { + self.file == other.file.as_deref() + && self.mappings == other.mappings.as_ref() + && self.sources == other.sources.as_ref() + && self.sources_content == other.sources_content.as_ref() + && self.names == other.names.as_ref() + && self.source_root == other.source_root.as_deref() + && self.ignore_list == other.ignore_list + } +} + +impl BorrowedSourceMap<'_> { + fn to_json(&self) -> Result { + let json = simd_json::serde::to_string(&self)?; + Ok(json) + } + + pub fn to_writer(&self, w: W) -> Result<()> { + simd_json::serde::to_writer(w, self)?; + Ok(()) + } +} + +type Owner = Vec; + +self_cell::self_cell!( + struct BorrowedValueCell { + owner: Owner, + + #[covariant] + dependent: BorrowedValue, + } +); + +self_cell::self_cell!( + struct StaticSourceMap { + owner: Arc, + + #[covariant] + dependent: BorrowedSourceMap, + } +); + +impl PartialEq for StaticSourceMap { + fn eq(&self, other: &Self) -> bool { + self.borrow_dependent() == other.borrow_dependent() + } +} + +impl Eq for StaticSourceMap {} + +impl Hash for StaticSourceMap { + fn hash(&self, state: &mut H) { + self.borrow_dependent().hash(state); + } +} + +impl StaticSourceMap { + fn from_borrowed_value_cell(cell: Arc) -> Self { + Self::new(cell, |owner| BorrowedSourceMap { + version: 3, + file: owner.borrow_dependent().get_str("file").map(Into::into), + sources: owner + .borrow_dependent() + .get_array("sources") + .map(|v| { + v.iter() + .map(|s| s.as_str().unwrap_or_default()) + .collect::>() + }) + .unwrap_or_default(), + sources_content: owner + .borrow_dependent() + .get_array("sourcesContent") + .map(|v| { + v.iter() + .map(|s| s.as_str().unwrap_or_default()) + .collect::>() + }) + .unwrap_or_default(), + names: owner + .borrow_dependent() + .get_array("names") + .map(|v| { + v.iter() + .map(|s| s.as_str().unwrap_or_default()) + .collect::>() + }) + .unwrap_or_default(), + mappings: owner + .borrow_dependent() + .get_str("mappings") + .unwrap_or_default(), + source_root: owner + .borrow_dependent() + .get_str("sourceRoot") + .map(Into::into), + debug_id: owner.borrow_dependent().get_str("debugId").map(Into::into), + ignore_list: owner.borrow_dependent().get_array("ignoreList").map(|v| { + v.iter() + .map(|n| n.as_u32().unwrap_or_default()) + .collect::>() + .into() + }), + }) + } + + pub fn from_json(json: String) -> Result { + Self::from_slice(json.into_bytes()) + } + + pub fn from_slice(slice: Vec) -> Result { + let borrowed_value_cell = BorrowedValueCell::try_new(slice, |owner| { + // We need a mutable slice from our owned data + // SAFETY: We're creating a mutable reference to the owned data. + // The self_cell ensures this reference is valid for the lifetime of the cell. + #[allow(unsafe_code)] + let bytes: &'static mut [u8] = unsafe { + std::slice::from_raw_parts_mut(owner.as_ptr().cast_mut(), owner.len()) + }; + simd_json::to_borrowed_value(bytes) + })?; + Ok(Self::from_borrowed_value_cell(Arc::new( + borrowed_value_cell, + ))) + } + + pub fn to_json(&self) -> Result { + self.borrow_dependent().to_json() + } + + pub fn to_writer(&self, w: W) -> Result<()> { + self.borrow_dependent().to_writer(w) + } +} + +#[derive(Clone, Eq)] +enum SourceMapCell { + Static(Arc), + Owned(OwnedSourceMap), +} + +impl PartialEq for SourceMapCell { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (SourceMapCell::Static(this), SourceMapCell::Static(other)) => { + this.borrow_dependent() == other.borrow_dependent() + } + (SourceMapCell::Static(this), SourceMapCell::Owned(other)) => { + this.borrow_dependent() == other + } + (SourceMapCell::Owned(this), SourceMapCell::Static(other)) => { + other.borrow_dependent() == this + } + (SourceMapCell::Owned(this), SourceMapCell::Owned(other)) => { + this == other + } + } + } +} + +impl Hash for SourceMapCell { + fn hash(&self, state: &mut H) { + match self { + SourceMapCell::Static(s) => s.hash(state), + SourceMapCell::Owned(owned) => owned.hash(state), + } + } +} + +/// Source map representation and utilities. +/// +/// This struct serves multiple purposes in the source mapping ecosystem: +/// +/// 1. **Source Map Generation**: Created by the `map()` method of various `Source` +/// implementations to provide mapping information between generated and original code +/// +/// 2. **JSON Deserialization**: Can be constructed from JSON strings via `from_json()`, +/// enabling integration with external source map files and `SourceMapSource` usage +/// +/// 3. **Caching Optimization**: Used by `CachedSource` to store computed source maps, +/// preventing expensive recomputation of mapping data during repeated access +/// +/// The source map follows the [Source Map Specification v3](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit) +/// and provides efficient serialization/deserialization capabilities. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct SourceMap(SourceMapCell); + +impl SourceMap { + /// Create a [SourceMap]. + pub fn new( + mappings: Mappings, + sources: Sources, + sources_content: SourcesContent, + names: Names, + ) -> Self + where + Mappings: Into>, + Sources: Into>, + SourcesContent: Into>, + Names: Into>, + { + Self(SourceMapCell::Owned(OwnedSourceMap { + version: 3, + file: None, + mappings: mappings.into(), + sources: sources.into(), + sources_content: sources_content.into(), + names: names.into(), + source_root: None, + debug_id: None, + ignore_list: None, + })) + } + + fn ensure_owned(&mut self) -> &mut OwnedSourceMap { + if matches!(self.0, SourceMapCell::Static(_)) { + let cell = match &self.0 { + SourceMapCell::Static(s) => s.with_dependent(|_, dependent| { + SourceMapCell::Owned(OwnedSourceMap { + version: dependent.version, + file: dependent.file.map(|s| s.to_string().into()), + sources: dependent + .sources + .iter() + .map(|s| s.to_string()) + .collect::>() + .into(), + sources_content: dependent + .sources_content + .iter() + .map(|s| s.to_string()) + .collect::>() + .into(), + names: dependent + .names + .iter() + .map(|s| s.to_string()) + .collect::>() + .into(), + mappings: dependent.mappings.to_string().into(), + source_root: dependent.source_root.map(|s| s.to_string().into()), + debug_id: dependent.debug_id.map(|s| s.to_string().into()), + ignore_list: dependent.ignore_list.clone(), + }) + }), + SourceMapCell::Owned(_) => unreachable!(), + }; + self.0 = cell; + } + match &mut self.0 { + SourceMapCell::Static(_) => { + unreachable!() + } + SourceMapCell::Owned(owned) => owned, + } + } + + /// Get the file field in [SourceMap]. + pub fn file(&self) -> Option<&str> { + match &self.0 { + SourceMapCell::Static(s) => s.borrow_dependent().file.as_ref().copied(), + SourceMapCell::Owned(owned) => owned.file.as_ref().map(|s| s.as_ref()), + } + } + + /// Set the file field in [SourceMap]. + pub fn set_file>>(&mut self, file: Option) { + self.ensure_owned().file = file.map(Into::into); + } + + /// Get the ignoreList field in [SourceMap]. + pub fn ignore_list(&self) -> Option<&Vec> { + match &self.0 { + SourceMapCell::Static(s) => s.borrow_dependent().ignore_list.as_deref(), + SourceMapCell::Owned(owned) => owned.ignore_list.as_deref(), + } + } + + /// Set the ignoreList field in [SourceMap]. + pub fn set_ignore_list>>>( + &mut self, + ignore_list: Option, + ) { + self.ensure_owned().ignore_list = ignore_list.map(Into::into); + } + + /// Get the decoded mappings in [SourceMap]. + pub fn decoded_mappings(&self) -> impl Iterator + '_ { + decode_mappings(self) + } + + /// Get the mappings string in [SourceMap]. + pub fn mappings(&self) -> &str { + match &self.0 { + SourceMapCell::Static(s) => s.borrow_dependent().mappings, + SourceMapCell::Owned(owned) => owned.mappings.as_ref(), + } + } + + /// Get the sources field in [SourceMap]. + pub fn sources(&self) -> Box + Send + '_> { + match &self.0 { + SourceMapCell::Static(s) => { + Box::new(s.borrow_dependent().sources.iter().copied()) + } + SourceMapCell::Owned(owned) => { + Box::new(owned.sources.iter().map(|s| s.as_str())) + } + } + } + + /// Set the sources field in [SourceMap]. + pub fn set_sources>>(&mut self, sources: T) { + self.ensure_owned().sources = sources.into() + } + + /// Get the source by index from sources field in [SourceMap]. + pub fn get_source(&self, index: usize) -> Option<&str> { + match &self.0 { + SourceMapCell::Static(s) => { + s.borrow_dependent().sources.get(index).copied() + } + SourceMapCell::Owned(owned) => { + owned.sources.get(index).map(AsRef::as_ref) + } + } + } + + /// Get the sourcesContent field in [SourceMap]. + pub fn sources_content(&self) -> Box + Send + '_> { + match &self.0 { + SourceMapCell::Static(s) => { + Box::new(s.borrow_dependent().sources_content.iter().copied()) + } + SourceMapCell::Owned(borrowed) => { + Box::new(borrowed.sources_content.iter().map(|s| s.as_str())) + } + } + } + + /// Set the sourcesContent field in [SourceMap]. + pub fn set_sources_content>>( + &mut self, + sources_content: T, + ) { + self.ensure_owned().sources_content = sources_content.into() + } + + /// Get the source content by index from sourcesContent field in [SourceMap]. + pub fn get_source_content(&self, index: usize) -> Option<&str> { + match &self.0 { + SourceMapCell::Static(s) => { + s.borrow_dependent().sources_content.get(index).copied() + } + SourceMapCell::Owned(owned) => { + owned.sources_content.get(index).map(AsRef::as_ref) + } + } + } + + /// Get the names field in [SourceMap]. + pub fn names(&self) -> Box + Send + '_> { + match &self.0 { + SourceMapCell::Static(s) => { + Box::new(s.borrow_dependent().names.iter().copied()) + } + SourceMapCell::Owned(owned) => { + Box::new(owned.names.iter().map(|s| s.as_str())) + } + } + } + + /// Set the names field in [SourceMap]. + pub fn set_names>>(&mut self, names: Vec) { + self.ensure_owned().names = names.into(); + } + + /// Get the name by index from names field in [SourceMap]. + pub fn get_name(&self, index: usize) -> Option<&str> { + match &self.0 { + SourceMapCell::Static(s) => { + s.borrow_dependent().names.get(index).copied() + } + SourceMapCell::Owned(owned) => owned.names.get(index).map(AsRef::as_ref), + } + } + + /// Get the source_root field in [SourceMap]. + pub fn source_root(&self) -> Option<&str> { + match &self.0 { + SourceMapCell::Static(s) => { + s.borrow_dependent().source_root.as_ref().map(|s| *s) + } + SourceMapCell::Owned(owned) => { + owned.source_root.as_ref().map(|s| s.as_ref()) + } + } + } + + /// Set the source_root field in [SourceMap]. + pub fn set_source_root>>(&mut self, source_root: Option) { + self.ensure_owned().source_root = source_root.map(Into::into); + } + + /// Set the debug_id field in [SourceMap]. + pub fn set_debug_id>>(&mut self, debug_id: Option) { + self.ensure_owned().debug_id = debug_id.map(Into::into); + } + + /// Get the debug_id field in [SourceMap]. + pub fn get_debug_id(&self) -> Option<&str> { + match &self.0 { + SourceMapCell::Static(s) => { + s.borrow_dependent().debug_id.as_ref().map(|s| *s) + } + SourceMapCell::Owned(owned) => { + owned.debug_id.as_ref().map(|s| s.as_ref()) + } + } + } +} + +impl std::fmt::Debug for SourceMap { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { + let indent = f.width().unwrap_or(0); + let indent_str = format!("{:indent$}", "", indent = indent); + + write!( + f, + "{indent_str}SourceMap::from_json({:?}.to_string()).unwrap()", + self.to_json().unwrap() + )?; + + Ok(()) + } +} + +impl SourceMap { + /// Create a [SourceMap] from json string. + pub fn from_json(json: impl Into) -> Result { + let s = StaticSourceMap::from_json(json.into())?; + Ok(SourceMap(SourceMapCell::Static(s.into()))) + } + + /// Create a [SourceMap] from reader. + pub fn from_reader(mut s: R) -> Result { + let mut json = String::default(); + s.read_to_string(&mut json)?; + Self::from_json(json) + } + + /// Creates a [SourceMap] from a byte slice containing JSON data. + pub fn from_slice(slice: impl Into>) -> Result { + let s = StaticSourceMap::from_slice(slice.into())?; + Ok(SourceMap(SourceMapCell::Static(s.into()))) + } + + /// Generate source map to a json string. + pub fn to_json(&self) -> Result { + match &self.0 { + SourceMapCell::Static(s) => s.to_json(), + SourceMapCell::Owned(owned) => owned.to_json(), + } + } + + /// Generate source map to writer. + pub fn to_writer(&self, w: W) -> Result<()> { + match &self.0 { + SourceMapCell::Static(s) => s.to_writer(w), + SourceMapCell::Owned(owned) => owned.to_writer(w), + } + } +} diff --git a/src/source_map_source.rs b/src/source_map_source.rs index 938f8d63..03f37b18 100644 --- a/src/source_map_source.rs +++ b/src/source_map_source.rs @@ -12,7 +12,7 @@ use crate::{ }; /// Options for [SourceMapSource::new]. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct SourceMapSourceOptions { /// The source code. pub value: V, @@ -30,7 +30,7 @@ pub struct SourceMapSourceOptions { /// An convenient options for [SourceMapSourceOptions], `original_source` and /// `inner_source_map` will be `None`, `remove_original_source` will be false. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct WithoutOriginalOptions { /// The source code. pub value: V, @@ -297,7 +297,7 @@ mod tests { let mut hasher = twox_hash::XxHash64::default(); sms1.hash(&mut hasher); - assert_eq!(format!("{:x}", hasher.finish()), "736934c6e249aa6e"); + assert_eq!(format!("{:x}", hasher.finish()), "36db7679a6e47037"); } #[test] @@ -306,7 +306,7 @@ mod tests { value: "hello world\n", name: "hello.txt", source_map: SourceMap::new( - "AAAA".to_string(), + "AAAA", vec!["".into()], vec!["".into()], vec![], @@ -321,7 +321,7 @@ mod tests { value: "hello world\n", name: "hello.txt", source_map: SourceMap::new( - "AAAA".to_string(), + "AAAA", vec!["hello-source.txt".into()], vec!["hello world\n".into()], vec![], @@ -388,7 +388,7 @@ mod tests { value: "hello\n", name: "a", source_map: SourceMap::new( - "AAAA;AACA".to_string(), + "AAAA;AACA", vec!["hello1".into()], vec![], vec![], @@ -398,7 +398,7 @@ mod tests { value: "hi", name: "b", source_map: SourceMap::new( - "AAAA,EAAE".to_string(), + "AAAA,EAAE", vec!["hello2".into()], vec![], vec![], @@ -408,7 +408,7 @@ mod tests { value: "hi", name: "b", source_map: SourceMap::new( - "AAAA,EAAE".to_string(), + "AAAA,EAAE", vec!["hello3".into()], vec![], vec![], @@ -417,12 +417,7 @@ mod tests { let c = SourceMapSource::new(WithoutOriginalOptions { value: "", name: "c", - source_map: SourceMap::new( - "AAAA".to_string(), - vec!["hello4".into()], - vec![], - vec![], - ), + source_map: SourceMap::new("AAAA", vec!["hello4".into()], vec![], vec![]), }); let source = ConcatSource::new([ a.clone(), @@ -791,9 +786,9 @@ mod tests { r#"SourceMapSource::new(SourceMapSourceOptions { value: "hello world", name: "hello.txt", - source_map: SourceMap::from_json("{\"version\":3,\"sources\":[\"hello.txt\"],\"names\":[],\"mappings\":\"AAAA,MAAG\"}").unwrap(), + source_map: SourceMap::from_json("{\"version\":3,\"sources\":[\"hello.txt\"],\"names\":[],\"mappings\":\"AAAA,MAAG\"}".to_string()).unwrap(), original_source: Some("你好 世界".to_string()), - inner_source_map: Some(SourceMap::from_json("{\"version\":3,\"sources\":[\"hello world.txt\"],\"sourcesContent\":[\"你好✋世界\"],\"names\":[],\"mappings\":\"AAAA,EAAE\"}").unwrap()), + inner_source_map: Some(SourceMap::from_json("{\"version\":3,\"sources\":[\"hello world.txt\"],\"sourcesContent\":[\"你好✋世界\"],\"names\":[],\"mappings\":\"AAAA,EAAE\"}".to_string()).unwrap()), remove_original_source: false, }).boxed()"# );