Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom css modules name patterns #180

Merged
merged 4 commits into from May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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