diff --git a/src/bundler.rs b/src/bundler.rs index 2226404d..557200cb 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -79,6 +79,7 @@ enum AtRuleParserValue<'a, T> { struct BundleStyleSheet<'i, T> { stylesheet: Option>, + source_map_source_index: Option, dependencies: Vec, css_modules_deps: Vec, parent_source_index: u32, @@ -316,6 +317,16 @@ where let mut stylesheet = StyleSheet::new(sources, CssRuleList(rules), self.options.clone()); + if self.source_map.is_some() { + stylesheet.source_map_source_indices = self + .stylesheets + .get_mut() + .unwrap() + .iter() + .map(|s| s.source_map_source_index) + .collect(); + } + stylesheet.source_map_urls = self .stylesheets .get_mut() @@ -415,6 +426,7 @@ where stylesheets.push(BundleStyleSheet { stylesheet: None, + source_map_source_index: None, layer: rule.layer.clone(), media: rule.media.clone(), supports: rule.supports.clone(), @@ -455,14 +467,15 @@ where StyleSheet::::parse_with(code, opts, at_rule_parser)? }; + let mut source_map_source_index = None; if let Some(source_map) = &self.source_map { - // Only add source if we don't have an input source map. - // If we do, this will be handled by the printer when remapping locations. - let sm = stylesheet.source_map_url(0); - if sm.is_none() || !sm.unwrap().starts_with("data") { + // Only omit the CSS source when the input source map can actually be read. + // Otherwise, the printer needs this index as a fallback for generated mappings. + if stylesheet.source_map(0).is_none() { let mut source_map = source_map.lock().unwrap(); let source_index = source_map.add_source(filename); let _ = source_map.set_source_content(source_index as usize, code); + source_map_source_index = Some(source_index); } } @@ -606,6 +619,7 @@ where let entry = &mut self.stylesheets.lock().unwrap()[source_index as usize]; entry.stylesheet = Some(stylesheet); + entry.source_map_source_index = source_map_source_index; entry.dependencies = dependencies?; entry.css_modules_deps = css_modules_deps?; diff --git a/src/printer.rs b/src/printer.rs index 9086b9f4..917875c3 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -72,6 +72,8 @@ pub struct Printer<'a, 'c, W> { #[cfg(feature = "sourcemap")] #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))] pub(crate) source_maps: Vec>, + #[cfg(feature = "sourcemap")] + pub(crate) source_map_source_indices: &'c [Option], pub(crate) loc: Location, indent: u8, line: u32, @@ -99,6 +101,8 @@ impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, 'c, W> { source_map: options.source_map, #[cfg(feature = "sourcemap")] source_maps: Vec::new(), + #[cfg(feature = "sourcemap")] + source_map_source_indices: &[], loc: Location { source_index: 0, line: 0, @@ -249,7 +253,12 @@ impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, 'c, W> { let mut original = OriginalLocation { original_line: loc.line, original_column: loc.column - 1, - source: loc.source_index, + source: self + .source_map_source_indices + .get(loc.source_index as usize) + .copied() + .flatten() + .unwrap_or(loc.source_index), name: None, }; diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 56dbdc2e..c6d974ee 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -80,6 +80,10 @@ pub struct StyleSheet<'i, T = DefaultAtRule> { pub sources: Vec, /// The source map URL extracted from the original style sheet. pub source_map_urls: Vec>, + /// Output source map indices corresponding to the bundler's source indices. + #[cfg(feature = "sourcemap")] + #[cfg_attr(feature = "serde", serde(skip))] + pub(crate) source_map_source_indices: Vec>, /// The license comments that appeared at the start of the file. pub license_comments: Vec>, /// A list of content hashes for all source files included within the style sheet. @@ -135,6 +139,8 @@ where StyleSheet { sources, source_map_urls: Vec::new(), + #[cfg(feature = "sourcemap")] + source_map_source_indices: Vec::new(), license_comments: Vec::new(), content_hashes: None, rules, @@ -202,6 +208,8 @@ where Ok(StyleSheet { sources: vec![options.filename.clone()], source_map_urls: vec![parser.current_source_map_url().map(|s| s.to_owned())], + #[cfg(feature = "sourcemap")] + source_map_source_indices: Vec::new(), content_hashes, rules, license_comments, @@ -280,6 +288,7 @@ where #[cfg(feature = "sourcemap")] if printer.source_map.is_some() { printer.source_maps = self.sources.iter().enumerate().map(|(i, _)| self.source_map(i)).collect(); + printer.source_map_source_indices = &self.source_map_source_indices; } for comment in &self.license_comments { diff --git a/tests/test_parcel_bundler_source_map.rs b/tests/test_parcel_bundler_source_map.rs new file mode 100644 index 00000000..198a2db1 --- /dev/null +++ b/tests/test_parcel_bundler_source_map.rs @@ -0,0 +1,58 @@ +#![cfg(all(feature = "bundler", feature = "sourcemap"))] + +use parcel_sourcemap::SourceMap; +use rspack_lightningcss::{ + bundler::{Bundler, ResolveResult, SourceProvider}, + stylesheet::{ParserOptions, PrinterOptions}, +}; +use std::{io, path::Path}; + +struct SingleFile(&'static str); + +impl SourceProvider for SingleFile { + type Error = io::Error; + + fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { + if file == Path::new("/entry.css") { + Ok(self.0) + } else { + Err(io::ErrorKind::NotFound.into()) + } + } + + fn resolve(&self, _: &str, _: &Path) -> Result { + Err(io::ErrorKind::NotFound.into()) + } +} + +fn assert_bundled_source(css: &'static str) { + let fs = SingleFile(css); + let mut map = SourceMap::new("/"); + let existing_index = map.add_source("/existing.css"); + map.set_source_content(existing_index as usize, ".existing {}").unwrap(); + + let mut bundler = Bundler::new(&fs, Some(&mut map), ParserOptions::default()); + let stylesheet = bundler.bundle(Path::new("/entry.css")).unwrap(); + stylesheet + .to_css(PrinterOptions { + source_map: Some(&mut map), + ..PrinterOptions::default() + }) + .unwrap(); + + let source_index = map.find_closest_mapping(0, 0).unwrap().original.unwrap().source; + assert_ne!(source_index, existing_index); + assert_eq!(map.get_source(source_index).unwrap(), "entry.css"); + assert_eq!(map.get_source_content(source_index).unwrap(), css); + assert_eq!(map.get_source_content(existing_index).unwrap(), ".existing {}"); +} + +#[test] +fn uses_the_output_maps_source_index() { + assert_bundled_source(".entry { color: red }"); +} + +#[test] +fn registers_css_when_the_inline_map_is_unreadable() { + assert_bundled_source(".entry { color: red }\n/*# sourceMappingURL=data:application/json,not-a-map */"); +}