From d710de84037e5dc052b97b6296f367eb9b6396f5 Mon Sep 17 00:00:00 2001 From: AsyncIter Date: Wed, 4 Nov 2026 15:05:15 +0800 Subject: [PATCH] feat: support custom source map implementations --- Cargo.lock | 1 + Cargo.toml | 8 +- benches/common/mod.rs | 38 ++- benches/transform.rs | 9 +- c/src/lib.rs | 7 +- examples/custom_at_rule.rs | 20 +- napi/src/lib.rs | 73 ++--- src/bundler.rs | 68 ++-- src/lib.rs | 399 ++++++++++++++++++------ src/main.rs | 16 +- src/printer.rs | 283 +++++++++++++++-- src/stylesheet.rs | 54 ++-- src/visitor.rs | 8 +- tests/test_bundler_source_map.rs | 205 ++++++++++++ tests/test_custom_parser.rs | 12 +- tests/test_parcel_bundler_source_map.rs | 5 +- 16 files changed, 951 insertions(+), 255 deletions(-) create mode 100644 tests/test_bundler_source_map.rs diff --git a/Cargo.lock b/Cargo.lock index ca119e8f..b3319900 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1531,6 +1531,7 @@ dependencies = [ "rspack-lightningcss-derive", "rspack-lightningcss-selectors", "rspack-lightningcss-static-self", + "rustc-hash", "schemars", "serde", "serde-content", diff --git a/Cargo.toml b/Cargo.toml index 38d4d136..a60b47ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ crate-type = ["rlib"] default = ["bundler", "nodejs", "sourcemap"] browserslist = ["browserslist-rs"] bundler = ["dashmap", "custom_sourcemap", "rayon"] -cli = ["clap", "serde_json", "browserslist", "jemallocator"] +cli = ["clap", "serde_json", "browserslist", "jemallocator", "sourcemap"] jsonschema = ["schemars", "serde", "rspack-lightningcss-selectors/jsonschema"] nodejs = ["dep:serde", "dep:serde-content"] serde = [ @@ -62,9 +62,8 @@ serde = [ "rspack-lightningcss-selectors/serde", "into_owned", ] -# Keep the existing feature name as a compatibility alias. -sourcemap = ["custom_sourcemap"] -custom_sourcemap = ["parcel_sourcemap"] +sourcemap = ["custom_sourcemap", "dep:parcel_sourcemap"] +custom_sourcemap = ["dep:rustc-hash"] visitor = [] into_owned = [ "rspack-lightningcss-static-self", @@ -89,6 +88,7 @@ lazy_static = "1.4.0" const-str = "1.1.0" pathdiff = "0.2.1" ahash = "0.8.7" +rustc-hash = { version = "2", optional = true } pastey = "0.1.0" indexmap = { version = "2.2.6", features = ["serde"] } # CLI deps diff --git a/benches/common/mod.rs b/benches/common/mod.rs index 86dfdd97..ac49d9b5 100644 --- a/benches/common/mod.rs +++ b/benches/common/mod.rs @@ -1,4 +1,5 @@ use codspeed_criterion_compat::black_box; +use parcel_sourcemap::SourceMap; use rspack_lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet}; use std::fmt::Write; @@ -37,14 +38,43 @@ pub fn transform_stylesheet(css: &str) { let mut stylesheet = StyleSheet::parse(css, ParserOptions::default()).unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); let result = stylesheet - .to_css(PrinterOptions { - minify: true, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + minify: true, + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap(); black_box(result); } +pub fn transform_stylesheet_with_source_map(css: &str) { + let mut source_map = SourceMap::new("/"); + let source_index = source_map.add_source("bench.css"); + source_map.set_source_content(source_index as usize, css).unwrap(); + + let mut stylesheet = StyleSheet::parse( + css, + ParserOptions { + source_index, + ..ParserOptions::default() + }, + ) + .unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + let result = stylesheet + .to_css( + PrinterOptions { + minify: true, + ..PrinterOptions::default() + }, + Some(&mut source_map), + ) + .unwrap(); + black_box((result, source_map)); +} + fn small_fixture() -> String { r#" :root { diff --git a/benches/transform.rs b/benches/transform.rs index 7d105b9e..100ec2b1 100644 --- a/benches/transform.rs +++ b/benches/transform.rs @@ -27,12 +27,19 @@ fn bench_stylesheet_transform(c: &mut Criterion) { for fixture in &fixtures { group.throughput(Throughput::Bytes(fixture.css.len() as u64)); group.bench_with_input( - BenchmarkId::from_parameter(fixture.name), + BenchmarkId::new("no-source-map", fixture.name), fixture.css.as_str(), |b, css| { b.iter(|| common::transform_stylesheet(black_box(css))); }, ); + group.bench_with_input( + BenchmarkId::new("source-map", fixture.name), + fixture.css.as_str(), + |b, css| { + b.iter(|| common::transform_stylesheet_with_source_map(black_box(css))); + }, + ); } group.finish(); diff --git a/c/src/lib.rs b/c/src/lib.rs index c585448f..7109c5fa 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -338,7 +338,6 @@ pub extern "C" fn lightningcss_stylesheet_to_css( } else { Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.project_root).to_bytes()) }) }, - source_map: source_map.as_mut(), targets: if options.targets != Targets::default() { Some(options.targets.into()).into() } else { @@ -356,7 +355,11 @@ pub extern "C" fn lightningcss_stylesheet_to_css( }, }; - let res = unwrap!(wrapper.stylesheet.to_css(opts), error, ToCssResult::default()); + let res = unwrap!( + wrapper.stylesheet.to_css(opts, source_map.as_mut()), + error, + ToCssResult::default() + ); let map = if let Some(mut source_map) = source_map { if !options.input_source_map.is_null() { diff --git a/examples/custom_at_rule.rs b/examples/custom_at_rule.rs index 2e0fff47..05fe731f 100644 --- a/examples/custom_at_rule.rs +++ b/examples/custom_at_rule.rs @@ -42,14 +42,18 @@ fn main() { stylesheet.visit(&mut ApplyVisitor { rules: &style_rules }).unwrap(); let result = stylesheet - .to_css(PrinterOptions { - targets: Browsers { - chrome: Some(100 << 16), - ..Browsers::default() - } - .into(), - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + targets: Browsers { + chrome: Some(100 << 16), + ..Browsers::default() + } + .into(), + ..PrinterOptions::default() + }, + #[cfg(feature = "custom_sourcemap")] + None::<&mut ()>, + ) .unwrap(); println!("{}", result.code); } diff --git a/napi/src/lib.rs b/napi/src/lib.rs index 3247e24e..964b08c7 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -763,24 +763,26 @@ fn compile<'i>( unused_symbols: config.unused_symbols.clone().unwrap_or_default(), })?; - stylesheet.to_css(PrinterOptions { - minify: config.minify.unwrap_or_default(), - source_map: source_map.as_mut(), - project_root, - targets, - analyze_dependencies: if let Some(d) = &config.analyze_dependencies { - match d { - AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), - AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { - remove_imports: !c.preserve_imports, - }), - _ => None, - } - } else { - None + stylesheet.to_css( + PrinterOptions { + minify: config.minify.unwrap_or_default(), + project_root, + targets, + analyze_dependencies: if let Some(d) = &config.analyze_dependencies { + match d { + AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), + AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { + remove_imports: !c.preserve_imports, + }), + _ => None, + } + } else { + None + }, + pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), }, - pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), - })? + source_map.as_mut(), + )? }; let map = if let Some(mut source_map) = source_map { @@ -898,24 +900,26 @@ fn compile_bundle<'i, 'o, P: SourceProvider, F: FnOnce(&mut StyleSheet<'i, AtRul unused_symbols: config.unused_symbols.clone().unwrap_or_default(), })?; - stylesheet.to_css(PrinterOptions { - minify: config.minify.unwrap_or_default(), - source_map: source_map.as_mut(), - project_root, - targets, - analyze_dependencies: if let Some(d) = &config.analyze_dependencies { - match d { - AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), - AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { - remove_imports: !c.preserve_imports, - }), - _ => None, - } - } else { - None + stylesheet.to_css( + PrinterOptions { + minify: config.minify.unwrap_or_default(), + project_root, + targets, + analyze_dependencies: if let Some(d) = &config.analyze_dependencies { + match d { + AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), + AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { + remove_imports: !c.preserve_imports, + }), + _ => None, + } + } else { + None + }, + pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), }, - pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), - })? + source_map.as_mut(), + )? }; let map = if let Some(source_map) = &mut source_map { @@ -1022,7 +1026,6 @@ fn compile_attr<'i>( }); attr.to_css(PrinterOptions { minify: config.minify, - source_map: None, project_root: None, targets, analyze_dependencies: if config.analyze_dependencies { diff --git a/src/bundler.rs b/src/bundler.rs index 557200cb..83383ea0 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -19,7 +19,7 @@ //! }; //! //! let fs = FileProvider::new(); -//! let mut bundler = Bundler::new(&fs, None, ParserOptions::default()); +//! let mut bundler = Bundler::new(&fs, None::<&mut ()>, ParserOptions::default()); //! let stylesheet = bundler.bundle(Path::new("style.css")).unwrap(); //! ``` @@ -43,6 +43,7 @@ use crate::{ use crate::{ error::{Error, ParserError}, media_query::MediaList, + printer::SourceMap, rules::{ import::ImportRule, media::MediaRule, @@ -52,7 +53,6 @@ use crate::{ stylesheet::{ParserOptions, StyleSheet}, }; use dashmap::DashMap; -use parcel_sourcemap::SourceMap; use rayon::prelude::*; use std::{ collections::HashSet, @@ -63,8 +63,8 @@ use std::{ /// A Bundler combines a CSS file and all imported dependencies together into /// a single merged style sheet. -pub struct Bundler<'a, 's, P, T: AtRuleParser<'a>> { - source_map: Option>, +pub struct Bundler<'a, 's, P, T: AtRuleParser<'a>, S = ()> { + source_map: Option>, fs: &'a P, source_indexes: DashMap, stylesheets: Mutex>>, @@ -236,15 +236,15 @@ impl<'i, T: std::error::Error> BundleErrorKind<'i, T> { } } -impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P, DefaultAtRuleParser> { +impl<'a, 's, P: SourceProvider, S: SourceMap + Send> Bundler<'a, 's, P, DefaultAtRuleParser, S> { /// Creates a new Bundler using the given source provider. /// If a source map is given, the content of each source file included in the bundle will /// be added accordingly. pub fn new( fs: &'a P, - source_map: Option<&'s mut SourceMap>, + source_map: Option<&'s mut S>, options: ParserOptions<'a>, - ) -> Bundler<'a, 's, P, DefaultAtRuleParser> { + ) -> Bundler<'a, 's, P, DefaultAtRuleParser, S> { Bundler { source_map: source_map.map(Mutex::new), fs, @@ -256,7 +256,8 @@ impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P, DefaultAtRuleParser> { } } -impl<'a, 's, P: SourceProvider, T: AtRuleParser<'a> + Clone + Sync + Send> Bundler<'a, 's, P, T> +impl<'a, 's, P: SourceProvider, T: AtRuleParser<'a> + Clone + Sync + Send, S: SourceMap + Send> + Bundler<'a, 's, P, T, S> where T::AtRule: Sync + Send + ToCss + Clone, { @@ -265,7 +266,7 @@ where /// be added accordingly. pub fn new_with_at_rule_parser( fs: &'a P, - source_map: Option<&'s mut SourceMap>, + source_map: Option<&'s mut S>, options: ParserOptions<'a>, at_rule_parser: &'s mut T, ) -> Self { @@ -471,10 +472,10 @@ where if let Some(source_map) = &self.source_map { // 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() { + 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.set_source_content(source_index, code); source_map_source_index = Some(source_index); } } @@ -969,9 +970,9 @@ mod tests { ); fn bundle(fs: P, entry: &str) -> String { - let mut bundler = Bundler::new(&fs, None, ParserOptions::default()); + let mut bundler = Bundler::<_, _, ()>::new(&fs, None, ParserOptions::default()); let stylesheet = bundler.bundle(Path::new(entry)).unwrap(); - stylesheet.to_css(PrinterOptions::default()).unwrap().code + stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>).unwrap().code } fn bundle_css_module( @@ -988,7 +989,7 @@ mod tests { project_root: Option<&str>, pattern: &'static str, ) -> (String, CssModuleExports) { - let mut bundler = Bundler::new( + let mut bundler = Bundler::<_, _, ()>::new( &fs, None, ParserOptions { @@ -1003,16 +1004,19 @@ mod tests { let mut stylesheet = bundler.bundle(Path::new(entry)).unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); let res = stylesheet - .to_css(PrinterOptions { - project_root, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + project_root, + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap(); (res.code, res.exports.unwrap()) } fn bundle_custom_media(fs: P, entry: &str) -> String { - let mut bundler = Bundler::new( + let mut bundler = Bundler::<_, _, ()>::new( &fs, None, ParserOptions { @@ -1035,10 +1039,13 @@ mod tests { }) .unwrap(); stylesheet - .to_css(PrinterOptions { - targets, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + targets, + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap() .code } @@ -1048,7 +1055,7 @@ mod tests { entry: &str, maybe_cb: Option) -> ()>>, ) { - let mut bundler = Bundler::new(&fs, None, ParserOptions::default()); + let mut bundler = Bundler::<_, _, ()>::new(&fs, None, ParserOptions::default()); let res = bundler.bundle(Path::new(entry)); match res { Ok(_) => unreachable!(), @@ -2181,6 +2188,7 @@ mod tests { } #[test] + #[cfg(feature = "sourcemap")] fn test_source_map() { let source = r#".imported { content: "yay, file support!"; @@ -2212,11 +2220,13 @@ mod tests { let mut stylesheet = bundler.bundle(Path::new("/a.css")).unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); stylesheet - .to_css(PrinterOptions { - source_map: Some(&mut sm), - minify: true, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + minify: true, + ..PrinterOptions::default() + }, + Some(&mut sm), + ) .unwrap(); let map = sm.to_json(None).unwrap(); assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index 85c465b5..22298ca7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,6 +56,7 @@ mod tests { use crate::dependencies::Dependency; use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind, SelectorError}; use crate::parser::ParserFlags; + use crate::printer::{OriginalLocation, SourceMap}; use crate::properties::custom::Token; use crate::properties::Property; use crate::rules::CssRule; @@ -137,7 +138,7 @@ mod tests { if let Err(e) = stylesheet.minify(MinifyOptions::default()) { panic_with_test_error("test_with_options", "minify", source, e); } - let res = match stylesheet.to_css(PrinterOptions::default()) { + let res = match stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>) { Ok(res) => res, Err(e) => panic_with_test_error("test_with_options", "print", source, e), }; @@ -153,7 +154,7 @@ mod tests { if let Err(e) = stylesheet.minify(MinifyOptions::default()) { panic_with_test_error("test_with_printer_options", "minify", source, e); } - let res = match stylesheet.to_css(options) { + let res = match stylesheet.to_css(options, None::<&mut ()>) { Ok(res) => res, Err(e) => panic_with_test_error("test_with_printer_options", "print", source, e), }; @@ -188,11 +189,14 @@ mod tests { }) { panic_with_test_error("minify_test_with_options", "minify", source, e); } - let res = match stylesheet.to_css(PrinterOptions { - minify: true, - targets, - ..PrinterOptions::default() - }) { + let res = match stylesheet.to_css( + PrinterOptions { + minify: true, + targets, + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) { Ok(res) => res, Err(e) => panic_with_test_error("minify_test_with_options", "print", source, e), }; @@ -226,10 +230,13 @@ mod tests { }) { panic_with_test_error("prefix_test", "minify", source, e); } - let res = match stylesheet.to_css(PrinterOptions { - targets: targets.into(), - ..PrinterOptions::default() - }) { + let res = match stylesheet.to_css( + PrinterOptions { + targets: targets.into(), + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) { Ok(res) => res, Err(e) => panic_with_test_error("prefix_test", "print", source, e), }; @@ -282,10 +289,13 @@ mod tests { }) { panic_with_test_error("nesting_test_with_targets", "minify", source, e); } - let res = match stylesheet.to_css(PrinterOptions { - targets, - ..PrinterOptions::default() - }) { + let res = match stylesheet.to_css( + PrinterOptions { + targets, + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) { Ok(res) => res, Err(e) => panic_with_test_error("nesting_test_with_targets", "print", source, e), }; @@ -301,7 +311,7 @@ mod tests { if let Err(e) = stylesheet.minify(MinifyOptions::default()) { panic_with_test_error("nesting_test_no_targets", "minify", source, e); } - let res = match stylesheet.to_css(PrinterOptions::default()) { + let res = match stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>) { Ok(res) => res, Err(e) => panic_with_test_error("nesting_test_no_targets", "print", source, e), }; @@ -328,7 +338,7 @@ mod tests { ) .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + let res = stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>).unwrap(); let exports: Vec = res.exports.unwrap().into_keys().collect(); assert_eq!(exports, ["zebra", "apple", "mango"]); let references: Vec = res @@ -366,10 +376,13 @@ mod tests { if let Err(e) = stylesheet.minify(MinifyOptions::default()) { panic_with_test_error("css_modules_test", "minify", source, e); } - let res = match stylesheet.to_css(PrinterOptions { - minify, - ..Default::default() - }) { + let res = match stylesheet.to_css( + PrinterOptions { + minify, + ..Default::default() + }, + None::<&mut ()>, + ) { Ok(res) => res, Err(e) => panic_with_test_error("css_modules_test", "print", source, e), }; @@ -406,7 +419,7 @@ mod tests { }) { panic_with_test_error("custom_media_test", "minify", source, e); } - let res = match stylesheet.to_css(PrinterOptions::default()) { + let res = match stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>) { Ok(res) => res, Err(e) => panic_with_test_error("custom_media_test", "print", source, e), }; @@ -4303,10 +4316,13 @@ mod tests { }) .unwrap(); let res = stylesheet - .to_css(PrinterOptions { - targets, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + targets, + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap(); assert_eq!( res.code, @@ -28310,10 +28326,13 @@ mod tests { .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); let res = stylesheet - .to_css(PrinterOptions { - minify: true, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + minify: true, + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap(); assert_eq!(res.code, ".foo{color:#00f;& .bar{color:red}}"); @@ -29017,7 +29036,7 @@ mod tests { }, ) .unwrap(); - if let Err(err) = stylesheet.to_css(PrinterOptions::default()) { + if let Err(err) = stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>) { assert_eq!(err.kind, PrinterErrorKind::InvalidCssModulesPatternInGrid); } else { unreachable!() @@ -29475,10 +29494,13 @@ mod tests { ) .unwrap(); let res = stylesheet - .to_css(PrinterOptions { - project_root: Some(project_root), - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + project_root: Some(project_root), + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap(); assert_eq!( res.code, @@ -29517,14 +29539,17 @@ mod tests { .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); let res = stylesheet - .to_css(PrinterOptions { - targets: Browsers { - chrome: Some(95 << 16), - ..Browsers::default() - } - .into(), - ..Default::default() - }) + .to_css( + PrinterOptions { + targets: Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + } + .into(), + ..Default::default() + }, + None::<&mut ()>, + ) .unwrap(); assert_eq!( res.code, @@ -29581,15 +29606,18 @@ mod tests { let stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); let res = stylesheet - .to_css(PrinterOptions { - pseudo_classes: Some(PseudoClasses { - hover: Some("is-hovered"), - active: Some("is-active"), - focus_visible: Some("focus-visible"), - ..PseudoClasses::default() - }), - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + pseudo_classes: Some(PseudoClasses { + hover: Some("is-hovered"), + active: Some("is-active"), + focus_visible: Some("focus-visible"), + ..PseudoClasses::default() + }), + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap(); assert_eq!(res.code, expected); @@ -29615,13 +29643,16 @@ mod tests { ) .unwrap(); let res = stylesheet - .to_css(PrinterOptions { - pseudo_classes: Some(PseudoClasses { - hover: Some("is-hovered"), - ..PseudoClasses::default() - }), - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + pseudo_classes: Some(PseudoClasses { + hover: Some("is-hovered"), + ..PseudoClasses::default() + }), + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap(); assert_eq!(res.code, expected); } @@ -29702,7 +29733,7 @@ mod tests { ..MinifyOptions::default() }) .unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + let res = stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>).unwrap(); assert_eq!(res.code, expected); let source = r#" @@ -29728,7 +29759,7 @@ mod tests { ..MinifyOptions::default() }) .unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + let res = stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>).unwrap(); assert_eq!(res.code, expected); let source = r#" @@ -29775,14 +29806,17 @@ mod tests { }) .unwrap(); let res = stylesheet - .to_css(PrinterOptions { - targets: Browsers { - chrome: Some(95 << 16), - ..Browsers::default() - } - .into(), - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + targets: Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + } + .into(), + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap(); assert_eq!(res.code, expected); @@ -29824,7 +29858,7 @@ mod tests { ..MinifyOptions::default() }) .unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + let res = stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>).unwrap(); assert_eq!(res.code, expected); } @@ -31754,11 +31788,14 @@ mod tests { .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); let res = stylesheet - .to_css(PrinterOptions { - analyze_dependencies: Some(Default::default()), - minify: true, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + analyze_dependencies: Some(Default::default()), + minify: true, + ..PrinterOptions::default() + }, + None::<&mut ()>, + ) .unwrap(); assert_eq!(res.code, expected); let dependencies = res.dependencies.unwrap(); @@ -31779,10 +31816,13 @@ mod tests { fn dep_error_test(source: &str, error: PrinterErrorKind) { let stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap(); - let res = stylesheet.to_css(PrinterOptions { - analyze_dependencies: Some(Default::default()), - ..PrinterOptions::default() - }); + let res = stylesheet.to_css( + PrinterOptions { + analyze_dependencies: Some(Default::default()), + ..PrinterOptions::default() + }, + None::<&mut ()>, + ); match res { Err(e) => assert_eq!(e.kind, error), _ => unreachable!(), @@ -32642,11 +32682,15 @@ b\0 c'); ) .unwrap(); let result = stylesheet - .to_css(PrinterOptions { - minify: true, - analyze_dependencies: Some(Default::default()), - ..Default::default() - }) + .to_css( + PrinterOptions { + minify: true, + analyze_dependencies: Some(Default::default()), + ..Default::default() + }, + #[cfg(feature = "custom_sourcemap")] + None::<&mut ()>, + ) .unwrap(); let dependencies = result.dependencies.unwrap(); assert_eq!(dependencies.len(), 1); @@ -32661,7 +32705,7 @@ b\0 c'); } #[test] - #[cfg(feature = "custom_sourcemap")] + #[cfg(feature = "sourcemap")] fn test_url_quoting_source_map() { let stylesheet = StyleSheet::parse( r#" @@ -32677,11 +32721,13 @@ b\0 c'); .unwrap(); let mut sm = parcel_sourcemap::SourceMap::new("/"); let result = stylesheet - .to_css(PrinterOptions { - minify: true, - source_map: Some(&mut sm), - ..Default::default() - }) + .to_css( + PrinterOptions { + minify: true, + ..Default::default() + }, + Some(&mut sm), + ) .unwrap(); let column = result.code.find(".bar").unwrap() as u32; let mapping = sm.find_closest_mapping(0, column).unwrap(); @@ -32698,8 +32744,153 @@ b\0 c'); minify_test(".foo { z-index: -9999999 }", ".foo{z-index:-9999999}"); } + #[derive(Default)] + struct TestSourceMap { + sources: Vec, + names: Vec, + sources_content: Vec<(u32, String)>, + mappings: Vec<(u32, u32, Option)>, + } + + impl SourceMap for TestSourceMap { + fn add_source(&mut self, source: &str) -> u32 { + if let Some(index) = self.sources.iter().position(|s| s == source) { + index as u32 + } else { + self.sources.push(source.to_owned()); + (self.sources.len() - 1) as u32 + } + } + + fn add_name(&mut self, name: &str) -> u32 { + if let Some(index) = self.names.iter().position(|s| s == name) { + index as u32 + } else { + self.names.push(name.to_owned()); + (self.names.len() - 1) as u32 + } + } + + fn set_source_content(&mut self, source_index: u32, source_content: &str) { + self.sources_content.push((source_index, source_content.to_owned())); + } + + fn add_mapping(&mut self, generated_line: u32, generated_column: u32, original: Option) { + self.mappings.push((generated_line, generated_column, original)); + } + + fn from_data_url(_source_root: &str, _data_url: &str) -> Option { + None + } + + fn find_closest_mapping(&mut self, line: u32, column: u32) -> Option { + self + .mappings + .iter() + .rev() + .find(|(generated_line, generated_column, _)| (*generated_line, *generated_column) <= (line, column)) + .and_then(|(_, _, original)| *original) + } + + fn get_source(&self, source_index: u32) -> Option<&str> { + self.sources.get(source_index as usize).map(String::as_str) + } + + fn get_name(&self, name_index: u32) -> Option<&str> { + self.names.get(name_index as usize).map(String::as_str) + } + + fn get_source_content(&self, source_index: u32) -> Option<&str> { + self + .sources_content + .iter() + .find(|(index, _)| *index == source_index) + .map(|(_, content)| content.as_str()) + } + } + #[test] - #[cfg(feature = "custom_sourcemap")] + fn test_custom_source_map_remapped_content() { + use crate::printer::Printer; + + for content in [None, Some(""), Some(".original { color: red }")] { + let original = OriginalLocation { + original_line: 10, + original_column: 5, + source: 0, + name: Some(0), + }; + let input_map = || TestSourceMap { + sources: vec!["original.scss".into()], + names: vec!["original".into()], + sources_content: content.map(|content| vec![(0, content.into())]).unwrap_or_default(), + mappings: vec![(0, 0, Some(original))], + }; + let mut output = TestSourceMap::default(); + // Ensure the cache uses output indices, which may differ from input indices. + output.add_source("existing.css"); + let mut css = String::new(); + let mut printer = Printer::new(&mut css, PrinterOptions::default()).with_source_map(Some(&mut output)); + printer.source_maps = vec![Some(input_map()), Some(input_map())]; + for source_index in [0, 1] { + for line in 0..100 { + printer.add_mapping(Location { + source_index, + line, + column: 1, + }); + } + } + drop(printer); + + assert_eq!(output.sources, vec!["existing.css", "original.scss"]); + assert_eq!(output.names, vec!["original"]); + assert_eq!( + output.sources_content, + content.map(|content| vec![(1, content.into())]).unwrap_or_default() + ); + assert_eq!(output.mappings.len(), 200); + assert!(output + .mappings + .iter() + .all(|(_, _, location)| *location == Some(OriginalLocation { source: 1, ..original }))); + } + } + + #[test] + fn test_custom_source_map() { + let mut stylesheet = StyleSheet::parse(".foo { color: red }", ParserOptions::default()).unwrap(); + stylesheet.minify(MinifyOptions::default()).unwrap(); + + let mut source_map = TestSourceMap::default(); + let result = stylesheet + .to_css( + PrinterOptions { + minify: true, + ..PrinterOptions::default() + }, + Some(&mut source_map), + ) + .unwrap(); + + assert_eq!(result.code, ".foo{color:red}"); + assert_eq!( + source_map.mappings.first(), + Some(&( + 0, + 0, + Some(OriginalLocation { + original_line: 0, + original_column: 0, + source: 0, + name: None, + }) + )) + ); + } + + #[test] + #[cfg(feature = "sourcemap")] fn test_input_source_map() { let source = r#".imported { content: "yay, file support!"; @@ -32720,11 +32911,13 @@ b\0 c'); stylesheet.minify(MinifyOptions::default()).unwrap(); let mut sm = parcel_sourcemap::SourceMap::new("/"); stylesheet - .to_css(PrinterOptions { - source_map: Some(&mut sm), - minify: true, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + minify: true, + ..PrinterOptions::default() + }, + Some(&mut sm), + ) .unwrap(); let map = sm.to_json(None).unwrap(); assert_eq!( @@ -32734,7 +32927,7 @@ b\0 c'); } #[test] - #[cfg(feature = "custom_sourcemap")] + #[cfg(feature = "sourcemap")] fn test_source_maps_with_license_comments() { let source = r#"/*! a single line comment */ /*! @@ -32766,11 +32959,13 @@ b\0 c'); .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); stylesheet - .to_css(PrinterOptions { - source_map: Some(&mut sm), - minify: true, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + minify: true, + ..PrinterOptions::default() + }, + Some(&mut sm), + ) .unwrap(); let map = sm.to_json(None).unwrap(); assert_eq!( diff --git a/src/main.rs b/src/main.rs index cbb2ae9b..335a14e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -209,13 +209,15 @@ pub fn main() -> Result<(), std::io::Error> { .unwrap(); stylesheet - .to_css(PrinterOptions { - minify: cli_args.minify, - source_map: source_map.as_mut(), - project_root: Some(&project_root.to_string_lossy()), - targets, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + minify: cli_args.minify, + project_root: Some(&project_root.to_string_lossy()), + targets, + ..PrinterOptions::default() + }, + source_map.as_mut(), + ) .unwrap() }; diff --git a/src/printer.rs b/src/printer.rs index ca93cf6a..76d7a500 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -8,18 +8,210 @@ use crate::selector::SelectorList; use crate::targets::{Targets, TargetsWithSupportsScope}; use crate::vendor_prefix::VendorPrefix; use cssparser::{serialize_identifier, serialize_name}; +#[cfg(feature = "sourcemap")] +use parcel_sourcemap::SourceMap as ParcelSourceMap; #[cfg(feature = "custom_sourcemap")] -use parcel_sourcemap::{OriginalLocation, SourceMap}; +use rustc_hash::FxHashSet; use std::fmt::Write; +#[cfg(not(feature = "custom_sourcemap"))] +use std::marker::PhantomData; + +/// An original source location referenced by a generated source map mapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OriginalLocation { + /// The zero-based line number in the original source. + pub original_line: u32, + /// The zero-based column number in the original source. + pub original_column: u32, + /// The source index in the source map. + pub source: u32, + /// The optional name index in the source map. + pub name: Option, +} + +/// A source map implementation that can receive generated mappings and +/// provide input mappings for remapping. +#[cfg(feature = "custom_sourcemap")] +pub trait SourceMap { + /// Adds a source path and returns its source index. + fn add_source(&mut self, source: &str) -> u32; + + /// Adds a symbol name and returns its name index. + fn add_name(&mut self, name: &str) -> u32; + + /// Sets the original content for a source index. + fn set_source_content(&mut self, source_index: u32, source_content: &str); + + /// Adds a mapping from a generated position to an optional original location. + fn add_mapping(&mut self, generated_line: u32, generated_column: u32, original: Option); + + /// Reads an inline source map data URL. + fn from_data_url(source_root: &str, data_url: &str) -> Option + where + Self: Sized; + + /// Finds the closest original location for a generated position. + fn find_closest_mapping(&mut self, line: u32, column: u32) -> Option; + + /// Returns the source path for a source index. + fn get_source(&self, source_index: u32) -> Option<&str>; + + /// Returns the symbol name for a name index. + fn get_name(&self, name_index: u32) -> Option<&str>; + + /// Returns the source content for a source index. + fn get_source_content(&self, source_index: u32) -> Option<&str>; +} + +/// A source map placeholder used when custom source maps are disabled. +#[cfg(not(feature = "custom_sourcemap"))] +pub trait SourceMap {} + +#[cfg(not(feature = "custom_sourcemap"))] +impl SourceMap for () {} + +#[cfg(feature = "custom_sourcemap")] +impl SourceMap for () { + fn add_source(&mut self, _source: &str) -> u32 { + unreachable!() + } + + fn add_name(&mut self, _name: &str) -> u32 { + unreachable!() + } + + fn set_source_content(&mut self, _source_index: u32, _source_content: &str) { + unreachable!() + } + + fn add_mapping(&mut self, _generated_line: u32, _generated_column: u32, _original: Option) { + unreachable!() + } + + fn from_data_url(_source_root: &str, _data_url: &str) -> Option { + unreachable!() + } + + fn find_closest_mapping(&mut self, _line: u32, _column: u32) -> Option { + unreachable!() + } + + fn get_source(&self, _source_index: u32) -> Option<&str> { + unreachable!() + } + + fn get_name(&self, _name_index: u32) -> Option<&str> { + unreachable!() + } + + fn get_source_content(&self, _source_index: u32) -> Option<&str> { + unreachable!() + } +} + +#[cfg(feature = "custom_sourcemap")] +impl SourceMap for &mut T { + fn add_source(&mut self, source: &str) -> u32 { + (**self).add_source(source) + } + + fn add_name(&mut self, name: &str) -> u32 { + (**self).add_name(name) + } + + fn set_source_content(&mut self, source_index: u32, source_content: &str) { + (**self).set_source_content(source_index, source_content) + } + + fn add_mapping(&mut self, generated_line: u32, generated_column: u32, original: Option) { + (**self).add_mapping(generated_line, generated_column, original) + } + + fn from_data_url(source_root: &str, data_url: &str) -> Option + where + Self: Sized, + { + let _ = (source_root, data_url); + None + } + + fn find_closest_mapping(&mut self, line: u32, column: u32) -> Option { + (**self).find_closest_mapping(line, column) + } + + fn get_source(&self, source_index: u32) -> Option<&str> { + (**self).get_source(source_index) + } + + fn get_name(&self, name_index: u32) -> Option<&str> { + (**self).get_name(name_index) + } + + fn get_source_content(&self, source_index: u32) -> Option<&str> { + (**self).get_source_content(source_index) + } +} + +#[cfg(feature = "sourcemap")] +#[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))] +impl SourceMap for ParcelSourceMap { + fn add_source(&mut self, source: &str) -> u32 { + ParcelSourceMap::add_source(self, source) + } + + fn add_name(&mut self, name: &str) -> u32 { + ParcelSourceMap::add_name(self, name) + } + + fn set_source_content(&mut self, source_index: u32, source_content: &str) { + let _ = ParcelSourceMap::set_source_content(self, source_index as usize, source_content); + } + + fn add_mapping(&mut self, generated_line: u32, generated_column: u32, original: Option) { + ParcelSourceMap::add_mapping( + self, + generated_line, + generated_column, + original.map(|original| parcel_sourcemap::OriginalLocation { + original_line: original.original_line, + original_column: original.original_column, + source: original.source, + name: original.name, + }), + ); + } + + fn from_data_url(source_root: &str, data_url: &str) -> Option { + ParcelSourceMap::from_data_url(source_root, data_url).ok() + } + + fn find_closest_mapping(&mut self, line: u32, column: u32) -> Option { + let mapping = ParcelSourceMap::find_closest_mapping(self, line, column)?; + mapping.original.map(|original| OriginalLocation { + original_line: original.original_line, + original_column: original.original_column, + source: original.source, + name: original.name, + }) + } + + fn get_source(&self, source_index: u32) -> Option<&str> { + ParcelSourceMap::get_source(self, source_index).ok() + } + + fn get_name(&self, name_index: u32) -> Option<&str> { + ParcelSourceMap::get_name(self, name_index).ok() + } + + fn get_source_content(&self, source_index: u32) -> Option<&str> { + ParcelSourceMap::get_source_content(self, source_index).ok() + } +} /// Options that control how CSS is serialized to a string. pub struct PrinterOptions<'a> { /// Whether to minify the CSS, i.e. remove white space. pub minify: bool, - /// An optional reference to a source map to write mappings into. - #[cfg(feature = "custom_sourcemap")] - #[cfg_attr(docsrs, doc(cfg(feature = "custom_sourcemap")))] - pub source_map: Option<&'a mut SourceMap>, /// An optional project root path, used to generate relative paths for sources used in CSS module hashes. pub project_root: Option<&'a str>, /// Targets to output the CSS for. @@ -41,8 +233,6 @@ impl<'a> Default for PrinterOptions<'a> { fn default() -> Self { PrinterOptions { minify: false, - #[cfg(feature = "custom_sourcemap")] - source_map: None, project_root: None, targets: Targets::default(), analyze_dependencies: None, @@ -91,23 +281,32 @@ pub struct PseudoClasses<'a> { /// /// `Printer` also includes helper functions that assist with writing output /// that respects options such as `minify`, and `css_modules`. -pub struct Printer<'a, 'c, W> { +pub struct Printer<'a, 'c, W, S: SourceMap = ()> { pub(crate) sources: Option<&'c Vec>, dest: &'a mut W, pub(crate) options: PrinterOptions<'a>, pub(crate) state: PrinterState<'a, 'c>, #[cfg(feature = "custom_sourcemap")] - pub(crate) source_map: Option<&'a mut SourceMap>, + pub(crate) source_map: Option<&'a mut S>, #[cfg(feature = "custom_sourcemap")] - pub(crate) source_maps: Vec>, + pub(crate) source_maps: Vec>, #[cfg(feature = "custom_sourcemap")] pub(crate) source_map_source_indices: &'c [Option], + #[cfg(feature = "custom_sourcemap")] + sources_with_content: FxHashSet, + #[cfg(not(feature = "custom_sourcemap"))] + source_map: PhantomData S>, } -impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, 'c, W> { +impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, 'c, W, ()> { /// Create a new Printer wrapping the given destination. - #[cfg_attr(not(feature = "custom_sourcemap"), allow(unused_mut))] - pub fn new(dest: &'a mut W, mut options: PrinterOptions<'a>) -> Self { + pub fn new(dest: &'a mut W, options: PrinterOptions<'a>) -> Self { + Self::new_impl(dest, options) + } +} + +impl<'a, 'c, W: std::fmt::Write + Sized, S: SourceMap> Printer<'a, 'c, W, S> { + fn new_impl(dest: &'a mut W, options: PrinterOptions<'a>) -> Self { let dependencies = if options.analyze_dependencies.is_some() { Some(Vec::new()) } else { @@ -118,7 +317,7 @@ impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, 'c, W> { sources: None, dest, #[cfg(feature = "custom_sourcemap")] - source_map: options.source_map.take(), + source_map: None, state: PrinterState { loc: Location { source_index: 0, @@ -141,6 +340,25 @@ impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, 'c, W> { source_maps: Vec::new(), #[cfg(feature = "custom_sourcemap")] source_map_source_indices: &[], + #[cfg(feature = "custom_sourcemap")] + sources_with_content: FxHashSet::default(), + #[cfg(not(feature = "custom_sourcemap"))] + source_map: PhantomData, + } + } + + #[cfg(feature = "custom_sourcemap")] + /// Attaches a source map writer to this printer. + pub fn with_source_map(self, source_map: Option<&'a mut T>) -> Printer<'a, 'c, W, T> { + Printer { + sources: self.sources, + dest: self.dest, + options: self.options, + state: self.state, + source_map, + source_maps: Vec::new(), + source_map_source_indices: &[], + sources_with_content: FxHashSet::default(), } } @@ -328,23 +546,22 @@ impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, 'c, W> { // Remap using input source map if possible. if let Some(Some(sm)) = self.source_maps.get_mut(loc.source_index as usize) { let mut found_mapping = false; - if let Some(mapping) = sm.find_closest_mapping(loc.line, loc.column - 1) { - if let Some(orig) = mapping.original { - let sources_len = map.get_sources().len(); - let source_index = map.add_source(sm.get_source(orig.source).unwrap()); - let name = orig.name.map(|name| map.add_name(sm.get_name(name).unwrap())); - original.original_line = orig.original_line; - original.original_column = orig.original_column; - original.source = source_index; - original.name = name; - - if map.get_sources().len() > sources_len { - let content = sm.get_source_content(orig.source).unwrap().to_owned(); - let _ = map.set_source_content(source_index as usize, &content); + if let Some(orig) = sm.find_closest_mapping(loc.line, loc.column - 1) { + let source_index = map.add_source(sm.get_source(orig.source).unwrap()); + let name = orig.name.map(|name| map.add_name(sm.get_name(name).unwrap())); + original.original_line = orig.original_line; + original.original_column = orig.original_column; + original.source = source_index; + original.name = name; + + if !self.sources_with_content.contains(&source_index) { + if let Some(content) = sm.get_source_content(orig.source) { + map.set_source_content(source_index, content); + self.sources_with_content.insert(source_index); } - - found_mapping = true; } + + found_mapping = true; } if !found_mapping { @@ -352,7 +569,7 @@ impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, 'c, W> { } } - map.add_mapping(self.state.generated_line, self.state.generated_col, Some(original)); + map.add_mapping(self.state.generated_line, self.state.generated_col, Some(original)) } } @@ -388,7 +605,7 @@ impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, 'c, W> { } } -impl<'a, 'c, W: std::fmt::Write + Sized> Write for Printer<'a, 'c, W> { +impl<'a, 'c, W: std::fmt::Write + Sized, S: SourceMap> Write for Printer<'a, 'c, W, S> { fn write_str(&mut self, s: &str) -> std::fmt::Result { let mut last_line_start = 0; for (idx, c) in s.char_indices() { @@ -493,7 +710,7 @@ pub trait PrinterTrait: std::fmt::Write + private::Sealed { fn without_css_module(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result; } -impl private::Sealed for Printer<'_, '_, W> { +impl private::Sealed for Printer<'_, '_, W, S> { fn targets(&self) -> &TargetsWithSupportsScope { &self.state.targets } @@ -583,7 +800,7 @@ impl private::Sealed for Printer<'_, '_, W> { } } -impl<'a, 'c, W: std::fmt::Write + Sized> PrinterTrait for Printer<'a, 'c, W> { +impl<'a, 'c, W: std::fmt::Write + Sized, S: SourceMap> PrinterTrait for Printer<'a, 'c, W, S> { fn options(&self) -> &PrinterOptions<'_> { &self.options } diff --git a/src/stylesheet.rs b/src/stylesheet.rs index e492c5b8..45d545d8 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -12,6 +12,8 @@ use crate::dependencies::Dependency; use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterError, PrinterErrorKind}; use crate::parser::{DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser}; use crate::printer::Printer; +#[cfg(feature = "custom_sourcemap")] +use crate::printer::SourceMap; use crate::rules::{CssRule, CssRuleList, MinifyContext}; use crate::targets::{should_compile, Targets, TargetsWithSupportsScope}; use crate::traits::{AtRuleParser, ToCss}; @@ -19,8 +21,6 @@ use crate::values::string::CowArcStr; #[cfg(feature = "visitor")] use crate::visitor::{Visit, VisitTypes, Visitor}; use cssparser::{Parser, ParserInput, StyleSheetParser}; -#[cfg(feature = "custom_sourcemap")] -use parcel_sourcemap::SourceMap; use std::collections::{HashMap, HashSet}; use std::fmt::Write; @@ -58,7 +58,7 @@ pub use crate::printer::PseudoClasses; /// stylesheet.minify(MinifyOptions::default()).unwrap(); /// /// // Serialize it to a string. -/// let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); +/// let res = stylesheet.to_css(PrinterOptions::default(), None::<&mut ()>).unwrap(); /// assert_eq!(res.code, ".foo, .bar {\n color: red;\n}\n"); /// ``` #[derive(Debug, Clone)] @@ -226,9 +226,8 @@ where /// Returns the inline source map associated with the source at the given index. #[cfg(feature = "custom_sourcemap")] - #[cfg_attr(docsrs, doc(cfg(feature = "custom_sourcemap")))] - pub fn source_map(&self, source_index: usize) -> Option { - SourceMap::from_data_url("/", self.source_map_url(source_index)?).ok() + pub fn source_map(&self, source_index: usize) -> Option { + S::from_data_url("/", self.source_map_url(source_index)?) } /// Minify and transform the style sheet for the provided browser targets. @@ -276,20 +275,39 @@ where } /// Serialize the style sheet to a CSS string. - pub fn to_css(&self, options: PrinterOptions) -> Result> { + #[cfg(feature = "custom_sourcemap")] + pub fn to_css<'a, S: SourceMap>( + &self, + options: PrinterOptions<'a>, + source_map: Option<&'a mut S>, + ) -> Result> { + self.to_css_impl(options, source_map) + } + + /// Serialize the style sheet to a CSS string. + #[cfg(not(feature = "custom_sourcemap"))] + pub fn to_css<'a>(&self, options: PrinterOptions<'a>) -> Result> { + self.to_css_impl(options) + } + + fn to_css_impl<'a, #[cfg(feature = "custom_sourcemap")] S: SourceMap>( + &self, + options: PrinterOptions<'a>, + #[cfg(feature = "custom_sourcemap")] source_map: Option<&'a mut S>, + ) -> Result> { // Make sure we always have capacity > 0: https://github.com/napi-rs/napi-rs/issues/1124. let mut dest = String::with_capacity(1); let project_root = options.project_root.clone(); - let mut printer = Printer::new(&mut dest, options); - + let printer = Printer::new(&mut dest, options); #[cfg(feature = "custom_sourcemap")] - { - printer.sources = Some(&self.sources); - } + let mut printer = printer.with_source_map(source_map); + #[cfg(not(feature = "custom_sourcemap"))] + let mut printer = printer; + printer.sources = Some(&self.sources); #[cfg(feature = "custom_sourcemap")] if printer.source_map.is_some() { - printer.source_maps = self.sources.iter().enumerate().map(|(i, _)| self.source_map(i)).collect(); + printer.source_maps = self.sources.iter().enumerate().map(|(i, _)| self.source_map::(i)).collect(); printer.source_map_source_indices = &self.source_map_source_indices; } @@ -313,7 +331,7 @@ where printer.newline()?; Ok(ToCssResult { - dependencies: printer.take_dependencies(), + dependencies: printer.state.dependencies, exports: Some(std::mem::take( &mut printer.state.css_module.unwrap().exports_by_source_index[0], )), @@ -325,7 +343,7 @@ where printer.newline()?; Ok(ToCssResult { - dependencies: printer.take_dependencies(), + dependencies: printer.state.dependencies, code: dest, exports: None, references: None, @@ -408,12 +426,6 @@ impl<'i> StyleAttribute<'i> { /// Serializes the style attribute to a CSS string. pub fn to_css(&self, options: PrinterOptions) -> Result { - #[cfg(feature = "custom_sourcemap")] - assert!( - options.source_map.is_none(), - "Source maps are not supported for style attributes" - ); - // Make sure we always have capacity > 0: https://github.com/napi-rs/napi-rs/issues/1124. let mut dest = String::with_capacity(1); let mut printer = Printer::new(&mut dest, options); diff --git a/src/visitor.rs b/src/visitor.rs index f5275227..fee5241d 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -54,7 +54,13 @@ //! //! stylesheet.visit(&mut MyVisitor).unwrap(); //! -//! let res = stylesheet.to_css(PrinterOptions { minify: true, ..Default::default() }).unwrap(); +//! let res = stylesheet +//! .to_css( +//! PrinterOptions { minify: true, ..Default::default() }, +//! #[cfg(feature = "custom_sourcemap")] +//! None::<&mut ()>, +//! ) +//! .unwrap(); //! assert_eq!(res.code, ".foo{background:url(https://mywebsite.com/bg.png);width:2rem}"); //! ``` diff --git a/tests/test_bundler_source_map.rs b/tests/test_bundler_source_map.rs new file mode 100644 index 00000000..a375c97c --- /dev/null +++ b/tests/test_bundler_source_map.rs @@ -0,0 +1,205 @@ +#![cfg(feature = "bundler")] + +use rspack_lightningcss::{ + bundler::{Bundler, ResolveResult, SourceProvider}, + printer::{OriginalLocation, SourceMap}, + rules::CssRule, + stylesheet::{ParserOptions, PrinterOptions}, +}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +struct TestProvider(HashMap); + +impl SourceProvider for TestProvider { + type Error = std::io::Error; + + fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { + Ok(self.0.get(file).unwrap()) + } + + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + Ok(originating_file.with_file_name(specifier).into()) + } +} + +// This test writer supports one inline map fixture. +const INPUT_MAP: &str = r#"data:application/json,{"version":3,"sources":["/original.scss"],"sourcesContent":[".original{color:red}"],"names":[],"mappings":"AAYG"}"#; + +#[derive(Default)] +struct TestSourceMap { + sources: Vec<(u32, String, Option)>, + mappings: Vec, + input_mapping: Option, +} + +impl SourceMap for TestSourceMap { + fn add_source(&mut self, source: &str) -> u32 { + // Simulate a writer that canonicalizes and deduplicates source paths. + let source = if source == "/alias.css" { + "/dependency.css" + } else { + source + }; + if let Some((index, _, _)) = self.sources.iter().find(|(_, path, _)| path == source) { + return *index; + } + + // Source indices need not be contiguous or start at zero. + let index = 100 + self.sources.len() as u32 * 10; + self.sources.push((index, source.into(), None)); + index + } + + fn add_name(&mut self, _: &str) -> u32 { + unreachable!() + } + + fn set_source_content(&mut self, source_index: u32, source_content: &str) { + self.sources.iter_mut().find(|(index, _, _)| *index == source_index).unwrap().2 = Some(source_content.into()); + } + + fn add_mapping(&mut self, _: u32, _: u32, original: Option) { + self.mappings.push(original.unwrap()); + } + + fn from_data_url(_: &str, data_url: &str) -> Option { + if data_url != INPUT_MAP { + return None; + } + + let mut map = Self::default(); + let source = map.add_source("/original.scss"); + map.set_source_content(source, ".original{color:red}"); + map.input_mapping = Some(OriginalLocation { + original_line: 12, + original_column: 3, + source, + name: None, + }); + Some(map) + } + + fn find_closest_mapping(&mut self, _: u32, _: u32) -> Option { + self.input_mapping + } + + fn get_source(&self, source_index: u32) -> Option<&str> { + self + .sources + .iter() + .find(|(index, _, _)| *index == source_index) + .map(|(_, source, _)| source.as_str()) + } + + fn get_name(&self, _: u32) -> Option<&str> { + None + } + + fn get_source_content(&self, source_index: u32) -> Option<&str> { + self + .sources + .iter() + .find(|(index, _, _)| *index == source_index) + .and_then(|(_, _, content)| content.as_deref()) + } +} + +#[test] +fn translates_bundler_source_indices() { + let entry = "@import 'dependency.css';\n@import 'alias.css';\n.entry { color: red }"; + let dependency = ".dependency { color: blue }"; + let fs = TestProvider(HashMap::from([ + (PathBuf::from("/entry.css"), entry.into()), + (PathBuf::from("/dependency.css"), dependency.into()), + (PathBuf::from("/alias.css"), dependency.into()), + ])); + + for prepopulated in [false, true] { + let mut map = TestSourceMap::default(); + if prepopulated { + let existing = map.add_source("/existing.css"); + map.set_source_content(existing, "existing content"); + map.add_source("/dependency.css"); + } + let mut bundler = Bundler::new(&fs, Some(&mut map), ParserOptions::default()); + let stylesheet = bundler.bundle(Path::new("/entry.css")).unwrap(); + + // Rule locations must still refer to the stylesheet's internal source list. + let rules: Vec<_> = stylesheet + .rules + .0 + .iter() + .filter_map(|rule| match rule { + CssRule::Style(rule) => Some(rule), + _ => None, + }) + .collect(); + assert_eq!(rules.len(), 3); + for (rule, source) in rules.iter().zip(["/dependency.css", "/alias.css", "/entry.css"]) { + assert_eq!(stylesheet.sources[rule.loc.source_index as usize], source); + } + + stylesheet.to_css(PrinterOptions::default(), Some(&mut map)).unwrap(); + assert_eq!(map.mappings.len(), 3); + for (mapping, (source, content)) in map.mappings.iter().zip([ + ("/dependency.css", dependency), + ("/dependency.css", dependency), + ("/entry.css", entry), + ]) { + assert_eq!(map.get_source(mapping.source), Some(source)); + assert_eq!(map.get_source_content(mapping.source), Some(content)); + } + assert_eq!(map.mappings[0].source, map.mappings[1].source); + if prepopulated { + assert_eq!(map.get_source_content(100), Some("existing content")); + } + } +} + +#[test] +fn translates_bundler_indices_alongside_inline_remapping() { + let dependency = ".dependency { color: blue }"; + let fs = TestProvider(HashMap::from([ + ( + PathBuf::from("/entry.css"), + format!("@import 'dependency.css';\n.entry {{ color: red }}\n/*# sourceMappingURL={INPUT_MAP} */"), + ), + (PathBuf::from("/dependency.css"), dependency.into()), + ])); + let mut map = TestSourceMap::default(); + map.add_source("/existing.css"); + let mut bundler = Bundler::new(&fs, Some(&mut map), ParserOptions::default()); + let stylesheet = bundler.bundle(Path::new("/entry.css")).unwrap(); + stylesheet.to_css(PrinterOptions::default(), Some(&mut map)).unwrap(); + + assert_eq!(map.mappings.len(), 2); + let dependency_mapping = map.mappings[0]; + assert_eq!(map.get_source(dependency_mapping.source), Some("/dependency.css")); + assert_eq!(map.get_source_content(dependency_mapping.source), Some(dependency)); + let original = map.mappings[1]; + assert_eq!(map.get_source(original.source), Some("/original.scss")); + assert_eq!(map.get_source_content(original.source), Some(".original{color:red}")); + assert_eq!((original.original_line, original.original_column), (12, 3)); + assert!(!map.sources.iter().any(|(_, source, _)| source == "/entry.css")); +} + +#[test] +fn registers_bundled_source_when_input_map_cannot_be_read() { + for source_map_url in ["data-bundle.map", "data:application/json,not-a-map"] { + let entry = format!(".entry {{ color: red }}\n/*# sourceMappingURL={source_map_url} */"); + let fs = TestProvider(HashMap::from([(PathBuf::from("/entry.css"), entry.clone())])); + let mut map = TestSourceMap::default(); + let mut bundler = Bundler::new(&fs, Some(&mut map), ParserOptions::default()); + let stylesheet = bundler.bundle(Path::new("/entry.css")).unwrap(); + stylesheet.to_css(PrinterOptions::default(), Some(&mut map)).unwrap(); + + assert_eq!(map.mappings.len(), 1); + let source = map.mappings[0].source; + assert_eq!(source, 100); + assert_eq!(map.get_source(source), Some("/entry.css")); + assert_eq!(map.get_source_content(source), Some(entry.as_str())); + } +} diff --git a/tests/test_custom_parser.rs b/tests/test_custom_parser.rs index ed03330e..cfbcb79e 100644 --- a/tests/test_custom_parser.rs +++ b/tests/test_custom_parser.rs @@ -13,10 +13,14 @@ fn minify_test(source: &str, expected: &str) { let mut stylesheet = StyleSheet::parse_with(source, ParserOptions::default(), &mut TestAtRuleParser).unwrap(); stylesheet.minify(Default::default()).unwrap(); let res = stylesheet - .to_css(PrinterOptions { - minify: true, - ..PrinterOptions::default() - }) + .to_css( + PrinterOptions { + minify: true, + ..PrinterOptions::default() + }, + #[cfg(feature = "custom_sourcemap")] + None::<&mut ()>, + ) .unwrap(); assert_eq!(res.code, expected); } diff --git a/tests/test_parcel_bundler_source_map.rs b/tests/test_parcel_bundler_source_map.rs index 198a2db1..8cd1bcf5 100644 --- a/tests/test_parcel_bundler_source_map.rs +++ b/tests/test_parcel_bundler_source_map.rs @@ -34,10 +34,7 @@ fn assert_bundled_source(css: &'static str) { 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() - }) + .to_css(PrinterOptions::default(), Some(&mut map)) .unwrap(); let source_index = map.find_closest_mapping(0, 0).unwrap().original.unwrap().source;