Skip to content

Commit

Permalink
feat: Add content-hash css module name pattern (#802)
Browse files Browse the repository at this point in the history
  • Loading branch information
rubberpants authored Aug 27, 2024
1 parent 0bcd896 commit 73d4cde
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 4 deletions.
1 change: 1 addition & 0 deletions c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions src/bundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -866,13 +883,23 @@ 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<P: SourceProvider>(
fs: P,
entry: &str,
project_root: Option<&str>,
pattern: &'static str,
) -> (String, CssModuleExports) {
let mut bundler = Bundler::new(
&fs,
None,
ParserOptions {
css_modules: Some(css_modules::Config {
dashed_idents: true,
pattern: css_modules::Pattern::parse(pattern).unwrap(),
..Default::default()
}),
..ParserOptions::default()
Expand Down Expand Up @@ -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]
Expand Down
56 changes: 54 additions & 2 deletions src/css_modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<W, E>(&self, hash: &str, path: &Path, local: &str, mut write: W) -> Result<(), E>
pub fn write<W, E>(
&self,
hash: &str,
path: &Path,
local: &str,
content_hash: &str,
mut write: W,
) -> Result<(), E>
where
W: FnMut(&str) -> Result<(), E>,
{
Expand All @@ -150,6 +163,9 @@ impl<'i> Pattern<'i> {
Segment::Hash => {
write(hash)?;
}
Segment::ContentHash => {
write(content_hash)?;
}
}
}
Ok(())
Expand All @@ -162,8 +178,9 @@ impl<'i> Pattern<'i> {
hash: &str,
path: &Path,
local: &str,
content_hash: &str,
) -> Result<String, std::fmt::Error> {
self.write(hash, path, local, |s| res.write_str(s))?;
self.write(hash, path, local, content_hash, |s| res.write_str(s))?;
Ok(res)
}
}
Expand All @@ -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.
Expand Down Expand Up @@ -245,6 +264,7 @@ pub(crate) struct CssModule<'a, 'b, 'c> {
pub config: &'a Config<'b>,
pub sources: Vec<&'c Path>,
pub hashes: Vec<String>,
pub content_hashes: &'a Option<Vec<String>>,
pub exports_by_source_index: Vec<CssModuleExports>,
pub references: &'a mut HashMap<String, CssModuleReference>,
}
Expand All @@ -255,6 +275,7 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> {
sources: &'c Vec<String>,
project_root: Option<&'c str>,
references: &'a mut HashMap<String, CssModuleReference>,
content_hashes: &'a Option<Vec<String>>,
) -> Self {
let project_root = project_root.map(|p| Path::new(p));
let sources: Vec<&Path> = sources.iter().map(|filename| Path::new(filename)).collect();
Expand All @@ -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,
}
}
Expand All @@ -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![],
Expand All @@ -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![],
Expand All @@ -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![],
Expand Down Expand Up @@ -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(),
)
Expand All @@ -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![],
Expand Down Expand Up @@ -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(),
},
Expand Down
22 changes: 22 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 24 additions & 2 deletions src/stylesheet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -81,6 +81,10 @@ pub struct StyleSheet<'i, 'o, T = DefaultAtRule> {
pub(crate) source_map_urls: Vec<Option<String>>,
/// The license comments that appeared at the start of the file.
pub license_comments: Vec<CowArcStr<'i>>,
/// A list of content hashes for all source files included within the style sheet.
/// This is only set if CSS modules are enabled and the pattern includes [content-hash].
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) content_hashes: Option<Vec<String>>,
#[cfg_attr(feature = "serde", serde(skip))]
/// The options the style sheet was originally parsed with.
options: ParserOptions<'o, 'i>,
Expand Down Expand Up @@ -135,6 +139,7 @@ where
sources,
source_map_urls: Vec::new(),
license_comments: Vec::new(),
content_hashes: None,
rules,
options,
}
Expand All @@ -150,6 +155,16 @@ where
let mut parser = Parser::new(&mut input);
let mut license_comments = Vec::new();

let mut content_hashes = None;
if let Some(config) = &options.css_modules {
if config.pattern.has_content_hash() {
content_hashes = Some(vec![hash(
&code,
matches!(config.pattern.segments[0], crate::css_modules::Segment::ContentHash),
)]);
}
}

let mut state = parser.state();
while let Ok(token) = parser.next_including_whitespace_and_comments() {
match token {
Expand Down Expand Up @@ -185,6 +200,7 @@ where
Ok(StyleSheet {
sources: vec![options.filename.clone()],
source_map_urls: vec![parser.current_source_map_url().map(|s| s.to_owned())],
content_hashes,
rules,
license_comments,
options,
Expand Down Expand Up @@ -271,7 +287,13 @@ where

if let Some(config) = &self.options.css_modules {
let mut references = HashMap::new();
printer.css_module = Some(CssModule::new(config, &self.sources, project_root, &mut references));
printer.css_module = Some(CssModule::new(
config,
&self.sources,
project_root,
&mut references,
&self.content_hashes,
));

self.rules.to_css(&mut printer)?;
printer.newline()?;
Expand Down
1 change: 1 addition & 0 deletions website/pages/css-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ The following placeholders are currently supported:

- `[name]` - The base name of the file, without the extension.
- `[hash]` - A hash of the full file path.
- `[content-hash]` - A hash of the file contents.
- `[local]` - The original class name or identifier.

<div class="warning">
Expand Down

0 comments on commit 73d4cde

Please sign in to comment.