From 73d4cde3b6a67b94a6d5b806c444345623927e5f Mon Sep 17 00:00:00 2001 From: Courtney Ferguson Date: Tue, 27 Aug 2024 17:13:29 -0600 Subject: [PATCH 001/127] feat: Add content-hash css module name pattern (#802) --- c/src/lib.rs | 1 + src/bundler.rs | 56 ++++++++++++++++++++++++++++++++++++ src/css_modules.rs | 56 ++++++++++++++++++++++++++++++++++-- src/lib.rs | 22 ++++++++++++++ src/printer.rs | 10 +++++++ src/stylesheet.rs | 26 +++++++++++++++-- website/pages/css-modules.md | 1 + 7 files changed, 168 insertions(+), 4 deletions(-) diff --git a/c/src/lib.rs b/c/src/lib.rs index ba689d96..759a18db 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -281,6 +281,7 @@ pub extern "C" fn lightningcss_stylesheet_parse( Some(lightningcss::css_modules::Config { pattern, dashed_idents: options.css_modules_dashed_idents, + ..Default::default() }) } else { None diff --git a/src/bundler.rs b/src/bundler.rs index d73545b8..00988487 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -293,6 +293,23 @@ where .flat_map(|s| s.stylesheet.as_ref().unwrap().license_comments.iter().cloned()) .collect(); + if let Some(config) = &self.options.css_modules { + if config.pattern.has_content_hash() { + stylesheet.content_hashes = Some( + self + .stylesheets + .get_mut() + .unwrap() + .iter() + .flat_map(|s| { + let s = s.stylesheet.as_ref().unwrap(); + s.content_hashes.as_ref().unwrap().iter().cloned() + }) + .collect(), + ); + } + } + Ok(stylesheet) } @@ -866,6 +883,15 @@ mod tests { fs: P, entry: &str, project_root: Option<&str>, + ) -> (String, CssModuleExports) { + bundle_css_module_with_pattern(fs, entry, project_root, "[hash]_[local]") + } + + fn bundle_css_module_with_pattern( + fs: P, + entry: &str, + project_root: Option<&str>, + pattern: &'static str, ) -> (String, CssModuleExports) { let mut bundler = Bundler::new( &fs, @@ -873,6 +899,7 @@ mod tests { ParserOptions { css_modules: Some(css_modules::Config { dashed_idents: true, + pattern: css_modules::Pattern::parse(pattern).unwrap(), ..Default::default() }), ..ParserOptions::default() @@ -1978,6 +2005,35 @@ mod tests { Some("/x/y/z"), ); assert_eq!(code, expected); + + let (code, _) = bundle_css_module_with_pattern( + TestProvider { + map: fs! { + "/a.css": r#" + @import "b.css"; + .a { color: red } + "#, + "/b.css": r#" + .a { color: green } + "# + }, + }, + "/a.css", + None, + "[content-hash]-[local]", + ); + assert_eq!( + code, + indoc! { r#" + .do5n2W-a { + color: green; + } + + .pP97eq-a { + color: red; + } + "#} + ); } #[test] diff --git a/src/css_modules.rs b/src/css_modules.rs index cfe33d71..07327315 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -107,6 +107,7 @@ impl<'i> Pattern<'i> { "[name]" => Segment::Name, "[local]" => Segment::Local, "[hash]" => Segment::Hash, + "[content-hash]" => Segment::ContentHash, s => return Err(PatternParseError::UnknownPlaceholder(s.into(), start_idx)), }; segments.push(segment); @@ -126,8 +127,20 @@ impl<'i> Pattern<'i> { Ok(Pattern { segments }) } + /// Whether the pattern contains any `[content-hash]` segments. + pub fn has_content_hash(&self) -> bool { + self.segments.iter().any(|s| matches!(s, Segment::ContentHash)) + } + /// Write the substituted pattern to a destination. - pub fn write(&self, hash: &str, path: &Path, local: &str, mut write: W) -> Result<(), E> + pub fn write( + &self, + hash: &str, + path: &Path, + local: &str, + content_hash: &str, + mut write: W, + ) -> Result<(), E> where W: FnMut(&str) -> Result<(), E>, { @@ -150,6 +163,9 @@ impl<'i> Pattern<'i> { Segment::Hash => { write(hash)?; } + Segment::ContentHash => { + write(content_hash)?; + } } } Ok(()) @@ -162,8 +178,9 @@ impl<'i> Pattern<'i> { hash: &str, path: &Path, local: &str, + content_hash: &str, ) -> Result { - self.write(hash, path, local, |s| res.write_str(s))?; + self.write(hash, path, local, content_hash, |s| res.write_str(s))?; Ok(res) } } @@ -181,6 +198,8 @@ pub enum Segment<'i> { Local, /// A hash of the file name. Hash, + /// A hash of the file contents. + ContentHash, } /// A referenced name within a CSS module, e.g. via the `composes` property. @@ -245,6 +264,7 @@ pub(crate) struct CssModule<'a, 'b, 'c> { pub config: &'a Config<'b>, pub sources: Vec<&'c Path>, pub hashes: Vec, + pub content_hashes: &'a Option>, pub exports_by_source_index: Vec, pub references: &'a mut HashMap, } @@ -255,6 +275,7 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { sources: &'c Vec, project_root: Option<&'c str>, references: &'a mut HashMap, + content_hashes: &'a Option>, ) -> Self { let project_root = project_root.map(|p| Path::new(p)); let sources: Vec<&Path> = sources.iter().map(|filename| Path::new(filename)).collect(); @@ -279,6 +300,7 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { exports_by_source_index: sources.iter().map(|_| HashMap::new()).collect(), sources, hashes, + content_hashes, references, } } @@ -295,6 +317,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], local, + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -314,6 +341,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], &local[2..], + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -336,6 +368,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], name, + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -365,6 +402,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[*source_index as usize], &self.sources[*source_index as usize], &name[2..], + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[*source_index as usize] + } else { + "" + }, ) .unwrap(), ) @@ -385,6 +427,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], &name[2..], + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), composes: vec![], @@ -427,6 +474,11 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { &self.hashes[source_index as usize], &self.sources[source_index as usize], name.0.as_ref(), + if let Some(content_hashes) = &self.content_hashes { + &content_hashes[source_index as usize] + } else { + "" + }, ) .unwrap(), }, diff --git a/src/lib.rs b/src/lib.rs index 02bf9fcf..e30dad5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24177,6 +24177,28 @@ mod tests { crate::css_modules::Config { ..Default::default() }, ); + css_modules_test( + r#" + .test { + composes: foo bar from "foo.css"; + background: white; + } + "#, + indoc! {r#" + ._5h2kwG-test { + background: #fff; + } + "#}, + map! { + "test" => "_5h2kwG-test" "foo" from "foo.css" "bar" from "foo.css" + }, + HashMap::new(), + crate::css_modules::Config { + pattern: crate::css_modules::Pattern::parse("[content-hash]-[local]").unwrap(), + ..Default::default() + }, + ); + // Stable hashes between project roots. fn test_project_root(project_root: &str, filename: &str, hash: &str) { let stylesheet = StyleSheet::parse( diff --git a/src/printer.rs b/src/printer.rs index d8c08e2d..235a8eab 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -276,6 +276,11 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { &css_module.hashes[self.loc.source_index as usize], &css_module.sources[self.loc.source_index as usize], ident, + if let Some(content_hashes) = &css_module.content_hashes { + &content_hashes[self.loc.source_index as usize] + } else { + "" + }, |s| { self.col += s.len() as u32; if first { @@ -306,6 +311,11 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { &css_module.hashes[self.loc.source_index as usize], &css_module.sources[self.loc.source_index as usize], &ident[2..], + if let Some(content_hashes) = &css_module.content_hashes { + &content_hashes[self.loc.source_index as usize] + } else { + "" + }, |s| { self.col += s.len() as u32; serialize_name(s, dest) diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 2848bf2b..7c332b77 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -4,7 +4,7 @@ //! A [StyleAttribute](StyleAttribute) represents an inline `style` attribute in HTML. use crate::context::{DeclarationContext, PropertyHandlerContext}; -use crate::css_modules::{CssModule, CssModuleExports, CssModuleReferences}; +use crate::css_modules::{hash, CssModule, CssModuleExports, CssModuleReferences}; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::dependencies::Dependency; use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterError, PrinterErrorKind}; @@ -81,6 +81,10 @@ pub struct StyleSheet<'i, 'o, T = DefaultAtRule> { pub(crate) source_map_urls: Vec