Skip to content

Commit

Permalink
Add support for custom css modules name patterns (#180)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed May 20, 2022
1 parent 417ea9e commit c7a59e1
Show file tree
Hide file tree
Showing 15 changed files with 370 additions and 130 deletions.
41 changes: 37 additions & 4 deletions node/src/lib.rs
Expand Up @@ -178,12 +178,25 @@ struct Config {
pub minify: Option<bool>,
pub source_map: Option<bool>,
pub drafts: Option<Drafts>,
pub css_modules: Option<bool>,
pub css_modules: Option<CssModulesOption>,
pub analyze_dependencies: Option<bool>,
pub pseudo_classes: Option<OwnedPseudoClasses>,
pub unused_symbols: Option<HashSet<String>>,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum CssModulesOption {
Bool(bool),
Config(CssModulesConfig),
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CssModulesConfig {
pattern: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BundleConfig {
Expand All @@ -192,7 +205,7 @@ struct BundleConfig {
pub minify: Option<bool>,
pub source_map: Option<bool>,
pub drafts: Option<Drafts>,
pub css_modules: Option<bool>,
pub css_modules: Option<CssModulesOption>,
pub analyze_dependencies: Option<bool>,
pub pseudo_classes: Option<OwnedPseudoClasses>,
pub unused_symbols: Option<HashSet<String>>,
Expand Down Expand Up @@ -237,7 +250,17 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result<TransformResult, Compil
ParserOptions {
nesting: matches!(drafts, Some(d) if d.nesting),
custom_media: matches!(drafts, Some(d) if d.custom_media),
css_modules: config.css_modules.unwrap_or(false),
css_modules: if let Some(css_modules) = &config.css_modules {
match css_modules {
CssModulesOption::Bool(true) => Some(parcel_css::css_modules::Config::default()),
CssModulesOption::Bool(false) => None,
CssModulesOption::Config(c) => Some(parcel_css::css_modules::Config {
pattern: parcel_css::css_modules::Pattern::parse(&c.pattern).unwrap(),
}),
}
} else {
None
},
source_index: 0,
},
)?;
Expand Down Expand Up @@ -288,7 +311,17 @@ fn compile_bundle<'i>(fs: &'i FileProvider, config: &BundleConfig) -> Result<Tra
let parser_options = ParserOptions {
nesting: matches!(drafts, Some(d) if d.nesting),
custom_media: matches!(drafts, Some(d) if d.custom_media),
css_modules: config.css_modules.unwrap_or(false),
css_modules: if let Some(css_modules) = &config.css_modules {
match css_modules {
CssModulesOption::Bool(true) => Some(parcel_css::css_modules::Config::default()),
CssModulesOption::Bool(false) => None,
CssModulesOption::Config(c) => Some(parcel_css::css_modules::Config {
pattern: parcel_css::css_modules::Pattern::parse(&c.pattern).unwrap(),
}),
}
} else {
None
},
..ParserOptions::default()
};

Expand Down
26 changes: 15 additions & 11 deletions src/bundler.rs
Expand Up @@ -54,17 +54,17 @@ 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> {
pub struct Bundler<'a, 'o, 's, P> {
source_map: Option<Mutex<&'s mut SourceMap>>,
fs: &'a P,
source_indexes: DashMap<PathBuf, u32>,
stylesheets: Mutex<Vec<BundleStyleSheet<'a>>>,
options: ParserOptions,
stylesheets: Mutex<Vec<BundleStyleSheet<'a, 'o>>>,
options: ParserOptions<'o>,
}

#[derive(Debug)]
struct BundleStyleSheet<'i> {
stylesheet: Option<StyleSheet<'i>>,
struct BundleStyleSheet<'i, 'o> {
stylesheet: Option<StyleSheet<'i, 'o>>,
dependencies: Vec<u32>,
parent_source_index: u32,
parent_dep_index: u32,
Expand Down Expand Up @@ -175,11 +175,11 @@ impl<'i> BundleErrorKind<'i> {
}
}

impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P> {
impl<'a, 'o, 's, P: SourceProvider> Bundler<'a, 'o, 's, P> {
/// 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>, options: ParserOptions) -> Self {
pub fn new(fs: &'a P, source_map: Option<&'s mut SourceMap>, options: ParserOptions<'o>) -> Self {
Bundler {
source_map: source_map.map(Mutex::new),
fs,
Expand All @@ -190,7 +190,7 @@ impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P> {
}

/// Bundles the given entry file and all dependencies into a single style sheet.
pub fn bundle<'e>(&mut self, entry: &'e Path) -> Result<StyleSheet<'a>, Error<BundleErrorKind<'a>>> {
pub fn bundle<'e>(&mut self, entry: &'e Path) -> Result<StyleSheet<'a, 'o>, Error<BundleErrorKind<'a>>> {
// Phase 1: load and parse all files. This is done in parallel.
self.load_file(
&entry,
Expand Down Expand Up @@ -413,7 +413,7 @@ impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P> {
fn order(&mut self) {
process(self.stylesheets.get_mut().unwrap(), 0, &mut HashSet::new());

fn process(stylesheets: &mut Vec<BundleStyleSheet<'_>>, source_index: u32, visited: &mut HashSet<u32>) {
fn process(stylesheets: &mut Vec<BundleStyleSheet>, source_index: u32, visited: &mut HashSet<u32>) {
if visited.contains(&source_index) {
return;
}
Expand All @@ -436,7 +436,11 @@ impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P> {
fn inline(&mut self, dest: &mut Vec<CssRule<'a>>) {
process(self.stylesheets.get_mut().unwrap(), 0, dest);

fn process<'a>(stylesheets: &mut Vec<BundleStyleSheet<'a>>, source_index: u32, dest: &mut Vec<CssRule<'a>>) {
fn process<'a>(
stylesheets: &mut Vec<BundleStyleSheet<'a, '_>>,
source_index: u32,
dest: &mut Vec<CssRule<'a>>,
) {
let stylesheet = &mut stylesheets[source_index as usize];
let mut rules = std::mem::take(&mut stylesheet.stylesheet.as_mut().unwrap().rules.0);

Expand Down Expand Up @@ -589,7 +593,7 @@ mod tests {
&fs,
None,
ParserOptions {
css_modules: true,
css_modules: Some(Default::default()),
..ParserOptions::default()
},
);
Expand Down
136 changes: 122 additions & 14 deletions src/css_modules.rs
Expand Up @@ -15,9 +15,108 @@ use data_encoding::{Encoding, Specification};
use lazy_static::lazy_static;
use parcel_selectors::SelectorList;
use serde::Serialize;
use smallvec::{smallvec, SmallVec};
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::fmt::Write;
use std::hash::{Hash, Hasher};
use std::path::Path;

/// Configuration for CSS modules.
#[derive(Default, Clone, Debug)]
pub struct Config<'i> {
/// The class name pattern to use. Default is `[hash]_[local]`.
pub pattern: Pattern<'i>,
}

/// A CSS modules class name pattern.
#[derive(Clone, Debug)]
pub struct Pattern<'i> {
/// The list of segments in the pattern.
pub segments: SmallVec<[Segment<'i>; 2]>,
}

impl<'i> Default for Pattern<'i> {
fn default() -> Self {
Pattern {
segments: smallvec![Segment::Hash, Segment::Literal("_"), Segment::Local],
}
}
}

impl<'i> Pattern<'i> {
/// Parse a pattern from a string.
pub fn parse(mut input: &'i str) -> Result<Self, ()> {
let mut segments = SmallVec::new();
while !input.is_empty() {
if input.starts_with('[') {
if let Some(end_idx) = input.find(']') {
let segment = match &input[0..=end_idx] {
"[name]" => Segment::Name,
"[local]" => Segment::Local,
"[hash]" => Segment::Hash,
_ => return Err(()),
};
segments.push(segment);
input = &input[end_idx + 1..];
} else {
return Err(());
}
} else {
let end_idx = input.find('[').unwrap_or_else(|| input.len());
segments.push(Segment::Literal(&input[0..end_idx]));
input = &input[end_idx..];
}
}

Ok(Pattern { segments })
}

/// Write the substituted pattern to a destination.
pub fn write<W, E>(&self, hash: &str, path: &Path, local: &str, mut write: W) -> Result<(), E>
where
W: FnMut(&str) -> Result<(), E>,
{
for segment in &self.segments {
match segment {
Segment::Literal(s) => {
write(s)?;
}
Segment::Name => {
write(path.file_stem().unwrap().to_str().unwrap())?;
}
Segment::Local => {
write(local)?;
}
Segment::Hash => {
write(hash)?;
}
}
}
Ok(())
}

fn write_to_string(&self, hash: &str, path: &Path, local: &str) -> Result<String, std::fmt::Error> {
let mut res = String::new();
self.write(hash, path, local, |s| res.write_str(s))?;
Ok(res)
}
}

/// A segment in a CSS modules class name pattern.
///
/// See [Pattern](Pattern).
#[derive(Clone, Debug)]
pub enum Segment<'i> {
/// A literal string segment.
Literal(&'i str),
/// The base file name.
Name,
/// The original class name.
Local,
/// A hash of the file name.
Hash,
}

/// A referenced name within a CSS module, e.g. via the `composes` property.
///
Expand Down Expand Up @@ -69,15 +168,26 @@ lazy_static! {
};
}

pub(crate) struct CssModule<'a> {
pub hash: &'a str,
pub(crate) struct CssModule<'a, 'b, 'c> {
pub config: &'a Config<'b>,
pub path: &'c Path,
pub hash: String,
pub exports: &'a mut CssModuleExports,
}

impl<'a> CssModule<'a> {
impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> {
pub fn new(config: &'a Config<'b>, filename: &'c str, exports: &'a mut CssModuleExports) -> Self {
Self {
config,
path: Path::new(filename),
hash: hash(filename, matches!(config.pattern.segments[0], Segment::Hash)),
exports,
}
}

pub fn add_local(&mut self, exported: &str, local: &str) {
self.exports.entry(exported.into()).or_insert_with(|| CssModuleExport {
name: get_hashed_name(self.hash, local),
name: self.config.pattern.write_to_string(&self.hash, &self.path, local).unwrap(),
composes: vec![],
is_referenced: false,
});
Expand All @@ -90,7 +200,7 @@ impl<'a> CssModule<'a> {
}
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(CssModuleExport {
name: get_hashed_name(self.hash, name),
name: self.config.pattern.write_to_string(&self.hash, &self.path, name).unwrap(),
composes: vec![],
is_referenced: true,
});
Expand All @@ -110,7 +220,11 @@ impl<'a> CssModule<'a> {
for name in &composes.names {
let reference = match &composes.from {
None => CssModuleReference::Local {
name: get_hashed_name(self.hash, name.0.as_ref()),
name: self
.config
.pattern
.write_to_string(&self.hash, &self.path, name.0.as_ref())
.unwrap(),
},
Some(ComposesFrom::Global) => CssModuleReference::Global {
name: name.0.as_ref().into(),
Expand Down Expand Up @@ -140,19 +254,13 @@ impl<'a> CssModule<'a> {
}
}

fn get_hashed_name(hash: &str, name: &str) -> String {
// Hash must come first so that CSS grid identifiers work.
// This is because grid lines may have an implicit -start or -end appended.
format!("{}_{}", hash, name)
}

pub(crate) fn hash(s: &str) -> String {
pub(crate) fn hash(s: &str, at_start: bool) -> String {
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
let hash = hasher.finish() as u32;

let hash = ENCODER.encode(&hash.to_le_bytes());
if matches!(hash.as_bytes()[0], b'0'..=b'9') {
if at_start && matches!(hash.as_bytes()[0], b'0'..=b'9') {
format!("_{}", hash)
} else {
hash
Expand Down
17 changes: 10 additions & 7 deletions src/declaration.rs
Expand Up @@ -52,9 +52,9 @@ pub struct DeclarationBlock<'i> {

impl<'i> DeclarationBlock<'i> {
/// Parses a declaration block from CSS syntax.
pub fn parse<'t>(
pub fn parse<'a, 'o, 't>(
input: &mut Parser<'i, 't>,
options: &ParserOptions,
options: &'a ParserOptions<'o>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let mut important_declarations = DeclarationList::new();
let mut declarations = DeclarationList::new();
Expand All @@ -79,7 +79,10 @@ impl<'i> DeclarationBlock<'i> {
}

/// Parses a declaration block from a string.
pub fn parse_string(input: &'i str, options: ParserOptions) -> Result<Self, ParseError<'i, ParserError<'i>>> {
pub fn parse_string<'o>(
input: &'i str,
options: ParserOptions<'o>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let mut input = ParserInput::new(input);
let mut parser = Parser::new(&mut input);
let result = Self::parse(&mut parser, &options)?;
Expand Down Expand Up @@ -371,14 +374,14 @@ impl<'i> DeclarationBlock<'i> {
}
}

struct PropertyDeclarationParser<'a, 'i> {
struct PropertyDeclarationParser<'a, 'o, 'i> {
important_declarations: &'a mut Vec<Property<'i>>,
declarations: &'a mut Vec<Property<'i>>,
options: &'a ParserOptions,
options: &'a ParserOptions<'o>,
}

/// Parse a declaration within {} block: `color: blue`
impl<'a, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, 'i> {
impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> {
type Declaration = ();
type Error = ParserError<'i>;

Expand All @@ -398,7 +401,7 @@ impl<'a, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a,
}

/// Default methods reject all at rules.
impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'i> {
impl<'a, 'o, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> {
type Prelude = ();
type AtRule = ();
type Error = ParserError<'i>;
Expand Down
2 changes: 1 addition & 1 deletion src/dependencies.rs
Expand Up @@ -87,7 +87,7 @@ pub struct UrlDependency {
impl UrlDependency {
/// Creates a new url dependency.
pub fn new(url: &Url, filename: &str) -> UrlDependency {
let placeholder = hash(&format!("{}_{}", filename, url.url));
let placeholder = hash(&format!("{}_{}", filename, url.url), false);
UrlDependency {
url: url.url.to_string(),
placeholder,
Expand Down

0 comments on commit c7a59e1

Please sign in to comment.