diff --git a/Cargo.lock b/Cargo.lock index b7dcb9c6fb9c..eb9fda64d8cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,6 +620,7 @@ dependencies = [ "biome_test_utils", "biome_unicode_table", "bitflags 2.5.0", + "bitvec", "enumflags2", "insta", "lazy_static", @@ -1098,6 +1099,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "bpaf" version = "0.9.9" @@ -1735,6 +1748,12 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.30" @@ -2888,6 +2907,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -3505,6 +3530,12 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "termcolor" version = "1.4.1" @@ -4413,6 +4444,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xorfilter-rs" version = "0.5.1" diff --git a/crates/biome_js_analyze/Cargo.toml b/crates/biome_js_analyze/Cargo.toml index 055b8f61cc36..5ead581e29b1 100644 --- a/crates/biome_js_analyze/Cargo.toml +++ b/crates/biome_js_analyze/Cargo.toml @@ -27,6 +27,7 @@ biome_string_case = { workspace = true } biome_suppression = { workspace = true } biome_unicode_table = { workspace = true } bitflags = { workspace = true } +bitvec = "1.0.1" enumflags2 = { workspace = true } lazy_static = { workspace = true } natord = { workspace = true } diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs index a0ac3b3d5170..9da4f46b64f3 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs @@ -17,15 +17,14 @@ use biome_js_factory::make::{ }; use biome_rowan::{AstNode, BatchMutationExt}; use lazy_static::lazy_static; +use presets::get_config_preset; use crate::JsRuleAction; pub use self::options::UtilityClassSortingOptions; use self::{ - any_class_string_like::AnyClassStringLike, - presets::{get_utilities_preset, UseSortedClassesPreset}, - sort::sort_class_name, - sort_config::SortConfig, + any_class_string_like::AnyClassStringLike, presets::UseSortedClassesPreset, + sort::sort_class_name, sort_config::SortConfig, }; declare_rule! { @@ -49,7 +48,7 @@ declare_rule! { /// /// Notably, keep in mind that the following features are not supported yet: /// - /// - Variant sorting. + /// - Variant sorting (arbitrary variants are not supported yet). /// - Custom utilitites and variants (such as ones introduced by Tailwind CSS plugins). Only the default Tailwind CSS configuration is supported. /// - Options such as `prefix` and `separator`. /// - Tagged template literals. @@ -147,10 +146,8 @@ declare_rule! { } lazy_static! { - static ref SORT_CONFIG: SortConfig = SortConfig::new( - get_utilities_preset(&UseSortedClassesPreset::default()), - Vec::new(), - ); + static ref SORT_CONFIG: SortConfig = + SortConfig::new(&get_config_preset(&UseSortedClassesPreset::default())); } impl Rule for UseSortedClasses { diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/class_info.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/class_info.rs index 0d01c58be6f7..238d366f5dfa 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/class_info.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/class_info.rs @@ -10,9 +10,13 @@ //! - The list of variants, in order of importance (which is used to compute the variants weight). //! - Other options, such as prefix and separator. +use std::{cmp::Ordering, collections::HashMap}; + +use bitvec::{order::Lsb0, vec::BitVec}; + use super::{ class_lexer::{tokenize_class, ClassSegmentStructure}, - sort_config::SortConfig, + sort_config::{build_variant_weight, SortConfig, VariantsConfig}, }; use crate::lint::nursery::use_sorted_classes::sort_config::UtilityLayer; @@ -30,9 +34,9 @@ enum UtilityMatch { None, } -impl UtilityMatch { +impl From<(&str, &str)> for UtilityMatch { /// Checks if a utility matches a target, and returns the result. - fn from(target: &str, utility_text: &str) -> UtilityMatch { + fn from((target, utility_text): (&str, &str)) -> UtilityMatch { // If the target ends with `$`, then it's an exact target. if target.ends_with('$') { // Check if the utility matches the target (without the final `$`) exactly. @@ -56,37 +60,40 @@ mod utility_match_tests { #[test] fn test_exact_match() { - assert_eq!(UtilityMatch::from("px-2$", "px-2"), UtilityMatch::Exact); + assert_eq!(UtilityMatch::from(("px-2$", "px-2")), UtilityMatch::Exact); // TODO: support negative values - // assert_eq!(UtilityMatch::from("px-2$", "-px-2"), UtilityMatch::Exact); - assert_eq!(UtilityMatch::from("px-2$", "not-px-2"), UtilityMatch::None); - assert_eq!(UtilityMatch::from("px-2$", "px-2-"), UtilityMatch::None); - assert_eq!(UtilityMatch::from("px-2$", "px-4"), UtilityMatch::None); - assert_eq!(UtilityMatch::from("px-2$", "px-2$"), UtilityMatch::None); - assert_eq!(UtilityMatch::from("px-2$", "px-2-"), UtilityMatch::None); - assert_eq!(UtilityMatch::from("px-2$", "px-2.5"), UtilityMatch::None); - assert_eq!(UtilityMatch::from("px-2$", "px-2.5$"), UtilityMatch::None); - assert_eq!(UtilityMatch::from("px-2$", "px-2.5-"), UtilityMatch::None); + // assert_eq!(UtilityMatch::from(("px-2$", "-px-2")), UtilityMatch::Exact); + assert_eq!( + UtilityMatch::from(("px-2$", "not-px-2")), + UtilityMatch::None + ); + assert_eq!(UtilityMatch::from(("px-2$", "px-2-")), UtilityMatch::None); + assert_eq!(UtilityMatch::from(("px-2$", "px-4")), UtilityMatch::None); + assert_eq!(UtilityMatch::from(("px-2$", "px-2$")), UtilityMatch::None); + assert_eq!(UtilityMatch::from(("px-2$", "px-2-")), UtilityMatch::None); + assert_eq!(UtilityMatch::from(("px-2$", "px-2.5")), UtilityMatch::None); + assert_eq!(UtilityMatch::from(("px-2$", "px-2.5$")), UtilityMatch::None); + assert_eq!(UtilityMatch::from(("px-2$", "px-2.5-")), UtilityMatch::None); } #[test] fn test_partial_match() { - assert_eq!(UtilityMatch::from("px-", "px-2"), UtilityMatch::Partial); + assert_eq!(UtilityMatch::from(("px-", "px-2")), UtilityMatch::Partial); // TODO: support negative values - // assert_eq!(UtilityMatch::from("px-", "-px-2"), UtilityMatch::Partial); - assert_eq!(UtilityMatch::from("px-", "px-2.5"), UtilityMatch::Partial); + // assert_eq!(UtilityMatch::from(("px-", "-px-2")), UtilityMatch::Partial); + assert_eq!(UtilityMatch::from(("px-", "px-2.5")), UtilityMatch::Partial); assert_eq!( - UtilityMatch::from("px-", "px-anything"), + UtilityMatch::from(("px-", "px-anything")), UtilityMatch::Partial ); assert_eq!( - UtilityMatch::from("px-", "px-%$>?+=-"), + UtilityMatch::from(("px-", "px-%$>?+=-")), UtilityMatch::Partial ); - assert_eq!(UtilityMatch::from("px-", "px-"), UtilityMatch::None); + assert_eq!(UtilityMatch::from(("px-", "px-")), UtilityMatch::None); // TODO: support negative values - // assert_eq!(UtilityMatch::from("px-", "-px-"), UtilityMatch::None); - assert_eq!(UtilityMatch::from("px-", "not-px-2"), UtilityMatch::None); + // assert_eq!(UtilityMatch::from(("px-", "-px-")), UtilityMatch::None); + assert_eq!(UtilityMatch::from(("px-", "not-px-2")), UtilityMatch::None); } } @@ -116,15 +123,15 @@ fn get_utility_info( } let utility_text = utility_data.text.as_str(); - let mut layer: &str = ""; + let mut layer: Option<&str> = None; let mut match_index: usize = 0; let mut last_size: usize = 0; // Iterate over each layer, looking for a match. for layer_data in utility_config.iter() { // Iterate over each target in the layer, looking for a match. - for (index, target) in layer_data.classes.iter().enumerate() { - match UtilityMatch::from(target, utility_text) { + for (index, &target) in layer_data.classes.iter().enumerate() { + match UtilityMatch::from((target, utility_text)) { UtilityMatch::Exact => { // Exact matches can be returned immediately. return Some(UtilityInfo { @@ -140,18 +147,18 @@ fn get_utility_info( // regardless of the order in which the targets are defined. let target_size = target.chars().count(); if target_size > last_size { - layer = layer_data.name; + layer = Some(layer_data.name); match_index = index; last_size = target_size; } } - _ => {} + UtilityMatch::None => {} } } } - if layer != "" { + if let Some(layer_match) = layer { return Some(UtilityInfo { - layer, + layer: layer_match, index: match_index, }); } @@ -275,6 +282,201 @@ mod get_utility_info_tests { } } +// variants +// ------- + +/// The result of matching a variant against a target. +#[derive(Debug, Eq, PartialEq)] +enum VariantMatch { + /// The variant matches an exact target. + Exact, + /// The variant matches a partial target. + Partial, + /// The variant does not match the target. + None, +} + +impl From<(&str, &str)> for VariantMatch { + /// Checks if a variant matches a target, and returns the result. + fn from((target, variant_text): (&str, &str)) -> VariantMatch { + // If the target matched exactly the variant text. + if target == variant_text { + return VariantMatch::Exact; + }; + + let variant_chars = variant_text.chars(); + let mut target_chars = target.chars(); + let mut target_found = true; + let mut dash_found = false; + let mut bracket_found = false; + // Checks if variant text has a custom value thus it starts with the target and it's followed by "-[" + for char in variant_chars { + match (char, target_chars.next()) { + (_, Some(target_char)) => { + if target_char != char { + target_found = false; + break; + } + } + ('-', None) => { + if target_found { + dash_found = true; + } + } + ('[', None) => { + if target_found && dash_found { + bracket_found = true; + } + } + (_, None) => { + break; + } + } + } + + if target_found && dash_found && bracket_found { + return VariantMatch::Exact; + } + + // Check if the variant starts with the (partial) target. + if variant_text.starts_with(target) && variant_text != target { + return VariantMatch::Partial; + } + // If all of the above checks fail, there is no match. + VariantMatch::None + } +} + +#[cfg(test)] +mod variant_match_tests { + use crate::lint::nursery::use_sorted_classes::class_info::VariantMatch; + + #[test] + fn test_exact_match() { + assert_eq!(VariantMatch::from(("hover", "hover")), VariantMatch::Exact); + assert_eq!(VariantMatch::from(("focus", "focus")), VariantMatch::Exact); + assert_eq!( + VariantMatch::from(("group", "group-[.is-published]")), + VariantMatch::Exact + ); + assert_eq!( + VariantMatch::from(("has", "has-[:checked]")), + VariantMatch::Exact + ); + assert_eq!( + VariantMatch::from(("group-has", "group-has-[.custom-class]")), + VariantMatch::Exact + ); + assert_eq!( + VariantMatch::from(("group-aria-disabled", "group-aria-disabled")), + VariantMatch::Exact + ); + } + + #[test] + fn test_partial_match() { + assert_eq!( + VariantMatch::from(("group", "group-has-[.custom-class]")), + VariantMatch::Partial + ); + assert_eq!( + VariantMatch::from(("peer", "peer-has-[:checked]")), + VariantMatch::Partial + ); + } + + #[test] + fn test_no_match() { + assert_eq!(VariantMatch::from(("group", "hover")), VariantMatch::None); + assert_eq!( + VariantMatch::from(("group-aria-busy", "group-aria-disabled")), + VariantMatch::None + ); + } +} + +fn find_variant_position(config_variants: VariantsConfig, variant_text: &str) -> Option { + let mut variant: Option<&str> = None; + let mut match_index: usize = 0; + let mut last_size: usize = 0; + + // Iterate over each variant looking for a match. + for (index, &target) in config_variants.iter().enumerate() { + match VariantMatch::from((target, variant_text)) { + VariantMatch::Exact => { + // Exact matches can be returned immediately. + return Some(index); + } + VariantMatch::Partial => { + // Multiple partial matches can occur, so we need to keep looking to find + // the longest target that matches. For example, if the variant text is + // `group-aria-[.custom-class]`, and there are targets like `group` and `group-aria`, we want to + // make sure that the `group-aria` target is matched as it is more specific, + // so when the target is `group` a Partial match will occur. + let target_size = target.chars().count(); + if target_size > last_size { + variant = Some(target); + match_index = index; + last_size = target_size; + } + } + VariantMatch::None => {} + } + } + if variant.is_some() { + return Some(match_index); + }; + None +} + +pub fn compute_variants_weight( + config_variants: VariantsConfig, + current_variants: &[&ClassSegmentStructure], +) -> Option> { + if current_variants.is_empty() { + return None; + }; + // Check if it's a known variant + // If it is then compute weights for each variant on the fly by using index as size + // TODO: Cache the weights for next run? + let mut variants_map: HashMap<&str, BitVec> = HashMap::new(); + for current_variant in current_variants.iter() { + let variant_name = current_variant.text.as_ref(); + let Some(variant_index) = find_variant_position(config_variants, variant_name) else { + continue; + }; + + if !variants_map.contains_key(variant_name) { + variants_map.insert(variant_name, build_variant_weight(variant_index)); + } + } + + // If there's a custom variant, their weight isn't important + if variants_map.is_empty() { + return None; + } + + // Compute Variants Weight as the BitWise XOR of all the recognized variants' weights + let variants_weight = variants_map + .iter() + .fold(BitVec::::new(), |acc, (_, val)| { + let mut accumulator = acc.clone(); + let mut current_weight = val.clone(); + let acc_len = accumulator.len(); + let current_weight_len = current_weight.len(); + + match acc_len.cmp(¤t_weight_len) { + Ordering::Less => accumulator.resize(current_weight_len, false), + Ordering::Greater => current_weight.resize(acc_len, false), + _ => (), + } + + accumulator ^ current_weight + }); + + Some(variants_weight) +} + // classes // ------- @@ -284,11 +486,13 @@ pub struct ClassInfo { /// The full text of the class itself. pub text: String, /// The total variants weight that results from the combination of all the variants. - pub variant_weight: Option, // TODO: this will need to be Option + pub variant_weight: Option>, /// The layer the utility belongs to. pub layer_index: usize, /// The index of the utility within the layer. pub utility_index: usize, + /// Arbitrary variants + pub arbitrary_variants: Option>, } /// Computes sort-related information about a CSS class. If the class is not recognized as a utility, @@ -296,17 +500,29 @@ pub struct ClassInfo { pub fn get_class_info(class_name: &str, sort_config: &SortConfig) -> Option { let utility_data = tokenize_class(class_name)?; let utility_info = get_utility_info(sort_config.utilities, &utility_data.utility); + + // Split up variants into arbitrary and known variants. + let (arbitrary_variants, current_variants): ( + Vec<&ClassSegmentStructure>, + Vec<&ClassSegmentStructure>, + ) = utility_data.variants.iter().partition(|el| el.arbitrary); + + let arbitrary_variants: Vec = arbitrary_variants + .iter() + .map(|&variant| variant.text.clone()) + .collect(); + if let Some(utility_info) = utility_info { return Some(ClassInfo { text: class_name.to_string(), - variant_weight: if utility_data.variants.is_empty() { + variant_weight: compute_variants_weight(sort_config.variants, ¤t_variants), + layer_index: *sort_config.layer_index_map.get(&utility_info.layer)?, + utility_index: utility_info.index, + arbitrary_variants: if arbitrary_variants.is_empty() { None } else { - // TODO: return None if there is an unknown variant. - Some(0) // TODO: actually compute variant weight + Some(arbitrary_variants) }, - layer_index: *sort_config.layer_index_map.get(&utility_info.layer)?, - utility_index: utility_info.index, }); } // If there is no utility info, the class is not recognized. @@ -315,22 +531,32 @@ pub fn get_class_info(class_name: &str, sort_config: &SortConfig) -> Option UtilitiesConfig { +pub struct ConfigPreset { + pub utilities: UtilitiesConfig, + pub variants: VariantsConfig, +} + +pub fn get_config_preset(preset: &UseSortedClassesPreset) -> ConfigPreset { match preset { - UseSortedClassesPreset::None => [].as_slice(), - UseSortedClassesPreset::TailwindCSS => TAILWIND_LAYERS.as_slice(), + UseSortedClassesPreset::None => get_empty_preset(), + UseSortedClassesPreset::TailwindCSS => get_tailwind_css_preset(), + } +} + +pub fn get_empty_preset() -> ConfigPreset { + ConfigPreset { + utilities: [].as_slice(), + variants: [].as_slice(), + } +} + +pub fn get_tailwind_css_preset() -> ConfigPreset { + ConfigPreset { + utilities: TAILWIND_LAYERS.as_slice(), + variants: VARIANT_CLASSES.as_slice(), } } diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort.rs index 3318d1780cb7..27bbf33c86e3 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort.rs @@ -9,17 +9,13 @@ use super::{ impl ClassInfo { /// Compare based on the existence of variants. Classes with variants go last. /// Returns `None` if both or none of the classes has variants. - fn cmp_has_variants(&self, other: &ClassInfo) -> Option { - if self.variant_weight.is_some() && other.variant_weight.is_some() { - return None; + fn cmp_variants_weight_existence(&self, other: &ClassInfo) -> Option { + match (&self.variant_weight, &other.variant_weight) { + (Some(_), Some(_)) => None, + (Some(_), _) => Some(Ordering::Greater), + (_, Some(_)) => Some(Ordering::Less), + (None, None) => None, } - if self.variant_weight.is_some() { - return Some(Ordering::Greater); - } - if other.variant_weight.is_some() { - return Some(Ordering::Less); - } - None } /// Compare based on layer indexes. Classes with lower indexes go first. @@ -32,10 +28,48 @@ impl ClassInfo { None } - /// Compare based on variants weight. Classes with higher weight go first. + /// Compare based on variants weight. Classes with lower weight go first. + /// First compare variants weight length. Only if their equal compare their actual weight. /// Returns `None` if they have the same weight. - fn cmp_variants_weight(&self, _other: &ClassInfo) -> Option { - // TODO: implement variant weight comparison. + fn cmp_variants_weight(&self, other: &ClassInfo) -> Option { + let current_weight = self.variant_weight.as_ref()?; + let other_weight = other.variant_weight.as_ref()?; + + let mut result = current_weight.len().cmp(&other_weight.len()); + if result == Ordering::Equal { + result = current_weight.cmp(other_weight); + } + + if result != Ordering::Equal { + return Some(result); + } + None + } + + /// Compare based on the existence of arbitrary variants. Classes with arbitrary variants go last. + /// Returns `None` if both or none of the classes has arbitrary variants. + fn cmp_arbitrary_variants_existence(&self, other: &ClassInfo) -> Option { + match (&self.arbitrary_variants, &other.arbitrary_variants) { + (Some(_), Some(_)) => None, + (Some(_), _) => Some(Ordering::Greater), + (_, Some(_)) => Some(Ordering::Less), + (None, None) => None, + } + } + + /// Compare arbitrary variants based on their length and then lexicographically + fn cmp_arbitrary_variants(&self, other: &ClassInfo) -> Option { + let a = self.arbitrary_variants.as_ref()?; + let b = other.arbitrary_variants.as_ref()?; + + let mut result = a.len().cmp(&b.len()); + if result == Ordering::Equal { + result = a.cmp(b); + } + + if result != Ordering::Equal { + return Some(result); + } None } @@ -56,8 +90,18 @@ impl ClassInfo { // This comparison function follows a very similar logic to the one in Tailwind CSS, with some // simplifications and necessary differences. fn compare_classes(a: &ClassInfo, b: &ClassInfo) -> Ordering { + // Classes with arbitrary variants go last + if let Some(has_arbitrary_variants) = a.cmp_arbitrary_variants_existence(b) { + return has_arbitrary_variants; + } + + // Compare arbitrary variants + if let Some(arbitrary_variants_order) = a.cmp_arbitrary_variants(b) { + return arbitrary_variants_order; + } + // Classes with variants go last. - if let Some(has_variants_order) = a.cmp_has_variants(b) { + if let Some(has_variants_order) = a.cmp_variants_weight_existence(b) { return has_variants_order; } diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort_config.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort_config.rs index d5edc96f4eb4..3de3cf64cf19 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort_config.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort_config.rs @@ -7,17 +7,29 @@ use std::collections::HashMap; +use bitvec::{order::Lsb0, vec::BitVec}; + +use super::presets::ConfigPreset; + /// A utility layer, containing its name and an ordered list of classes. pub struct UtilityLayer { pub name: &'static str, pub classes: &'static [&'static str], } +pub fn build_variant_weight(size: usize) -> BitVec { + let mut bit_vec = BitVec::new(); + let iterable = vec![false; size]; + bit_vec.extend(iterable); + bit_vec.push(true); + bit_vec +} + /// The utilities config, contains an ordered list of utility layers. pub type UtilitiesConfig = &'static [UtilityLayer]; /// The variants config, contains an ordered list of variants. -pub type VariantsConfig = Vec; +pub type VariantsConfig = &'static [&'static str]; /// The sort config, containing the utility config and the variant config. pub struct SortConfig { @@ -29,19 +41,19 @@ pub struct SortConfig { impl SortConfig { /// Creates a new sort config. - pub fn new(utilities_config: &'static [UtilityLayer], variants: VariantsConfig) -> Self { + pub fn new(preset: &ConfigPreset) -> Self { // Compute the layer index map. let mut layer_index_map: HashMap<&'static str, usize> = HashMap::new(); let mut index = 0; - for layer in utilities_config.iter() { + for layer in preset.utilities.iter() { layer_index_map.insert(layer.name, index); index += 1; } layer_index_map.insert("arbitrary", index); Self { - utilities: utilities_config, - variants, + utilities: preset.utilities, + variants: preset.variants, layer_index_map, } } diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/tailwind_preset.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/tailwind_preset.rs index 74b1423c52a4..61fc9d808a04 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/tailwind_preset.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/tailwind_preset.rs @@ -5,7 +5,7 @@ use super::sort_config::UtilityLayer; const COMPONENTS_LAYER_CLASSES: [&str; 1] = ["container$"]; -const UTILITIES_LAYER_CLASSES: [&str; 569] = [ +const UTILITIES_LAYER_CLASSES: [&str; 578] = [ "sr-only$", "not-sr-only$", "pointer-events-none$", @@ -520,6 +520,7 @@ const UTILITIES_LAYER_CLASSES: [&str; 569] = [ "mix-blend-saturation$", "mix-blend-color$", "mix-blend-luminosity$", + "mix-blend-plus-darker$", "mix-blend-plus-lighter$", "shadow$", "shadow-", @@ -572,11 +573,187 @@ const UTILITIES_LAYER_CLASSES: [&str; 569] = [ "duration-", "ease-", "will-change-", + "contain-none$", + "contain-content$", + "contain-strict$", + "contain-size$", + "contain-inline-size$", + "contain-layout$", + "contain-paint$", + "contain-style$", "content-", "forced-color-adjust-auto$", "forced-color-adjust-none$", ]; +pub const VARIANT_CLASSES: [&str; 165] = [ + "*", + "first-letter", + "first-line", + "marker", + "selection", + "file", + "placeholder", + "backdrop", + "before", + "after", + "first", + "last", + "only", + "odd", + "even", + "first-of-type", + "last-of-type", + "only-of-type", + "visited", + "target", + "open", + "default", + "checked", + "indeterminate", + "placeholder-shown", + "autofill", + "optional", + "required", + "valid", + "invalid", + "in-range", + "out-of-range", + "read-only", + "empty", + "focus-within", + "hover", + "focus", + "focus-visible", + "active", + "enabled", + "disabled", + "group-first", + "group-last", + "group-only", + "group-odd", + "group-even", + "group-first-of-type", + "group-last-of-type", + "group-only-of-type", + "group-visited", + "group-target", + "group-open", + "group-default", + "group-checked", + "group-indeterminate", + "group-placeholder-shown", + "group-autofill", + "group-optional", + "group-required", + "group-valid", + "group-invalid", + "group-in-range", + "group-out-of-range", + "group-read-only", + "group-empty", + "group-focus-within", + "group-hover", + "group-focus", + "group-focus-visible", + "group-active", + "group-enabled", + "group-disabled", + "group", + "peer-first", + "peer-last", + "peer-only", + "peer-odd", + "peer-even", + "peer-first-of-type", + "peer-last-of-type", + "peer-only-of-type", + "peer-visited", + "peer-target", + "peer-open", + "peer-default", + "peer-checked", + "peer-indeterminate", + "peer-placeholder-shown", + "peer-autofill", + "peer-optional", + "peer-required", + "peer-valid", + "peer-invalid", + "peer-in-range", + "peer-out-of-range", + "peer-read-only", + "peer-empty", + "peer-focus-within", + "peer-hover", + "peer-focus", + "peer-focus-visible", + "peer-active", + "peer-enabled", + "peer-disabled", + "peer", + "has", + "group-has", + "peer-has", + "aria-busy", + "aria-checked", + "aria-disabled", + "aria-expanded", + "aria-hidden", + "aria-pressed", + "aria-readonly", + "aria-required", + "aria-selected", + "aria", + "group-aria-busy", + "group-aria-checked", + "group-aria-disabled", + "group-aria-expanded", + "group-aria-hidden", + "group-aria-pressed", + "group-aria-readonly", + "group-aria-required", + "group-aria-selected", + "group-aria", + "peer-aria-busy", + "peer-aria-checked", + "peer-aria-disabled", + "peer-aria-expanded", + "peer-aria-hidden", + "peer-aria-pressed", + "peer-aria-readonly", + "peer-aria-required", + "peer-aria-selected", + "peer-aria", + "data", + "group-data", + "peer-data", + "supports", + "motion-safe", + "motion-reduce", + "contrast-more", + "contrast-less", + "max-sm", + "max-md", + "max-lg", + "max-xl", + "max-2xl", + "max", + "sm", + "md", + "lg", + "xl", + "2xl", + "min", + "portrait", + "landscape", + "ltr", + "rtl", + "dark", + "forced-colors", + "print", +]; + pub const TAILWIND_LAYERS: [UtilityLayer; 2] = [ UtilityLayer { name: "components", diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/sorted.jsx b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/sorted.jsx index 39401ef7b106..d6c2e4eae487 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/sorted.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/sorted.jsx @@ -15,6 +15,17 @@
+ {/* variant sorting */} +
+
+
+
+
+ {/* TODO: arbitrary variant */} +
+
+
+
; // functions diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/sorted.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/sorted.jsx.snap index 2bbcd2e369e6..95183ef67c82 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/sorted.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/sorted.jsx.snap @@ -21,6 +21,17 @@ expression: sorted.jsx
+ {/* variant sorting */} +
+
+
+
+
+ {/* TODO: arbitrary variant */} +
+
+
+
; // functions @@ -51,3 +62,28 @@ clsx({ }); ``` + +# Diagnostics +``` +sorted.jsx:25:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 23 │
+ 24 │ {/* TODO: arbitrary variant */} + > 25 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 26 │
+ 27 │
+ + i Unsafe fix: Sort the classes. + + 23 23 │
+ 24 24 │ {/* TODO: arbitrary variant */} + 25 │ - → + 25 │ + → + 26 26 │
+ 27 27 │
+ + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx index 95fcec344859..af26da713498 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx @@ -18,6 +18,18 @@
+ {/* variant sorting */} + {/* SHOULD emit diagnostics (arbitrary variants not supported yet) */} +
+
+
+
+
+ {/* TODO: arbitrary variant */} +
+
+
+
; // functions diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx.snap index c38e6708c7d5..9fa7c068d966 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx.snap @@ -24,6 +24,18 @@ expression: unsorted.jsx
+ {/* variant sorting */} + {/* SHOULD emit diagnostics (arbitrary variants not supported yet) */} +
+
+
+
+
+ {/* TODO: arbitrary variant */} +
+
+
+
; // functions @@ -311,7 +323,7 @@ unsorted.jsx:19:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━ > 19 │
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 20 │
- 21 │ ; + 21 │ {/* variant sorting */} i Unsafe fix: Sort the classes. @@ -320,7 +332,7 @@ unsorted.jsx:19:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━ 19 │ - → 19 │ + → 20 20 │
- 21 21 │ ; + 21 21 │ {/* variant sorting */} ``` @@ -334,8 +346,8 @@ unsorted.jsx:20:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━ 19 │
> 20 │
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 21 │ ; - 22 │ + 21 │ {/* variant sorting */} + 22 │ {/* SHOULD emit diagnostics (arbitrary variants not supported yet) */} i Unsafe fix: Sort the classes. @@ -343,224 +355,440 @@ unsorted.jsx:20:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━ 19 19 │
20 │ - → 20 │ + → - 21 21 │ ; - 22 22 │ + 21 21 │ {/* variant sorting */} + 22 22 │ {/* SHOULD emit diagnostics (arbitrary variants not supported yet) */} ``` ``` -unsorted.jsx:34:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unsorted.jsx:23:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! These CSS classes should be sorted. - 32 │ // nested values - 33 │ /* SHOULD emit diagnostics (class attribute supported by default) */ - > 34 │
; + 21 │ {/* variant sorting */} + 22 │ {/* SHOULD emit diagnostics (arbitrary variants not supported yet) */} + > 23 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 24 │
+ 25 │
+ + i Unsafe fix: Sort the classes. + + 21 21 │ {/* variant sorting */} + 22 22 │ {/* SHOULD emit diagnostics (arbitrary variants not supported yet) */} + 23 │ - → + 23 │ + → + 24 24 │
+ 25 25 │
+ + +``` + +``` +unsorted.jsx:24:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 22 │ {/* SHOULD emit diagnostics (arbitrary variants not supported yet) */} + 23 │
+ > 24 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 25 │
+ 26 │
+ + i Unsafe fix: Sort the classes. + + 22 22 │ {/* SHOULD emit diagnostics (arbitrary variants not supported yet) */} + 23 23 │
+ 24 │ - → + 24 │ + → + 25 25 │
+ 26 26 │
+ + +``` + +``` +unsorted.jsx:25:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 23 │
+ 24 │
+ > 25 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 26 │
+ 27 │
+ + i Unsafe fix: Sort the classes. + + 23 23 │
+ 24 24 │
+ 25 │ - → + 25 │ + → + 26 26 │
+ 27 27 │
+ + +``` + +``` +unsorted.jsx:26:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 24 │
+ 25 │
+ > 26 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 27 │
+ 28 │ {/* TODO: arbitrary variant */} + + i Unsafe fix: Sort the classes. + + 24 24 │
+ 25 25 │
+ 26 │ - → + 26 │ + → + 27 27 │
+ 28 28 │ {/* TODO: arbitrary variant */} + + +``` + +``` +unsorted.jsx:27:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 25 │
+ 26 │
+ > 27 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 28 │ {/* TODO: arbitrary variant */} + 29 │
+ + i Unsafe fix: Sort the classes. + + 25 25 │
+ 26 26 │
+ 27 │ - → + 27 │ + → + 28 28 │ {/* TODO: arbitrary variant */} + 29 29 │
+ + +``` + +``` +unsorted.jsx:29:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 27 │
+ 28 │ {/* TODO: arbitrary variant */} + > 29 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 30 │
+ 31 │
+ + i Unsafe fix: Sort the classes. + + 27 27 │
+ 28 28 │ {/* TODO: arbitrary variant */} + 29 │ - → + 29 │ + → + 30 30 │
+ 31 31 │
+ + +``` + +``` +unsorted.jsx:30:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 28 │ {/* TODO: arbitrary variant */} + 29 │
+ > 30 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 31 │
+ 32 │
+ + i Unsafe fix: Sort the classes. + + 28 28 │ {/* TODO: arbitrary variant */} + 29 29 │
+ 30 │ - → + 30 │ + → + 31 31 │
+ 32 32 │
+ + +``` + +``` +unsorted.jsx:31:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 29 │
+ 30 │
+ > 31 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 32 │
+ 33 │ ; + + i Unsafe fix: Sort the classes. + + 29 29 │
+ 30 30 │
+ 31 │ - → + 31 │ + → + 32 32 │
+ 33 33 │ ; + + +``` + +``` +unsorted.jsx:32:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 30 │
+ 31 │
+ > 32 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 33 │ ; + 34 │ + + i Unsafe fix: Sort the classes. + + 30 30 │
+ 31 31 │
+ 32 │ - → + 32 │ + → + 33 33 │ ; + 34 34 │ + + +``` + +``` +unsorted.jsx:46:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 44 │ // nested values + 45 │ /* SHOULD emit diagnostics (class attribute supported by default) */ + > 46 │
; │ ^^^^^^^^^^^^^^^^^^ - 35 │
; - 36 │
; + 47 │
; + 48 │
; i Unsafe fix: Sort the classes. - 32 32 │ // nested values - 33 33 │ /* SHOULD emit diagnostics (class attribute supported by default) */ - 34 │ - ; - 34 │ + ; - 35 35 │
; - 36 36 │
; + 44 44 │ // nested values + 45 45 │ /* SHOULD emit diagnostics (class attribute supported by default) */ + 46 │ - ; + 46 │ + ; + 47 47 │
; + 48 48 │
; ``` ``` -unsorted.jsx:35:14 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unsorted.jsx:47:14 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! These CSS classes should be sorted. - 33 │ /* SHOULD emit diagnostics (class attribute supported by default) */ - 34 │
; - > 35 │
; + 45 │ /* SHOULD emit diagnostics (class attribute supported by default) */ + 46 │
; + > 47 │
; │ ^^^^^^^^^^^^^^^^ - 36 │
; - 37 │
; + 48 │
; + 49 │
; i Unsafe fix: Sort the classes. - 33 33 │ /* SHOULD emit diagnostics (class attribute supported by default) */ - 34 34 │
; - 35 │ - ; - 35 │ + ; - 36 36 │
; - 37 37 │
; + 45 45 │ /* SHOULD emit diagnostics (class attribute supported by default) */ + 46 46 │
; + 47 │ - ; + 47 │ + ; + 48 48 │
; + 49 49 │
; ``` ``` -unsorted.jsx:36:14 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unsorted.jsx:48:14 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! These CSS classes should be sorted. - 34 │
; - 35 │
; - > 36 │
; + 46 │
; + 47 │
; + > 48 │
; │ ^^^^^^^^^^^^^^^^^^ - 37 │
; - 38 │
; + 50 │
; - 35 35 │
; - 36 │ - ; - 36 │ + ; - 37 37 │
; - 38 38 │
; + 47 47 │
; + 48 │ - ; + 48 │ + ; + 49 49 │
; + 50 50 │
; - 36 │
; - > 37 │
; + 47 │
; + 48 │
; + > 49 │
; │ ^^^^^^^^^^^^^^^^ - 38 │
; - 36 36 │
; - 37 │ - ; - 37 │ + ; - 38 38 │
; + 48 48 │
; + 49 │ - ; + 49 │ + ; + 50 50 │
40 │ "px-2 foo p-4 bar": [ + 50 │
52 │ "px-2 foo p-4 bar": [ │ ^^^^^^^^^^^^^^^^^^ - 41 │ "px-2 foo p-4 bar", - 42 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, + 53 │ "px-2 foo p-4 bar", + 54 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, i Unsafe fix: Sort the classes. - 38 38 │
41 │ "px-2 foo p-4 bar", + 51 │ class={{ + 52 │ "px-2 foo p-4 bar": [ + > 53 │ "px-2 foo p-4 bar", │ ^^^^^^^^^^^^^^^^^^ - 42 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, - 43 │ ], + 54 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, + 55 │ ], i Unsafe fix: Sort the classes. - 39 39 │ class={{ - 40 40 │ "px-2 foo p-4 bar": [ - 41 │ - → → → "px-2·foo·p-4·bar", - 41 │ + → → → "foo·bar·p-4·px-2", - 42 42 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, - 43 43 │ ], + 51 51 │ class={{ + 52 52 │ "px-2 foo p-4 bar": [ + 53 │ - → → → "px-2·foo·p-4·bar", + 53 │ + → → → "foo·bar·p-4·px-2", + 54 54 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, + 55 55 │ ], ``` ``` -unsorted.jsx:42:6 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unsorted.jsx:54:6 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! These CSS classes should be sorted. - 40 │ "px-2 foo p-4 bar": [ - 41 │ "px-2 foo p-4 bar", - > 42 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, + 52 │ "px-2 foo p-4 bar": [ + 53 │ "px-2 foo p-4 bar", + > 54 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, │ ^^^^^^^^^^^^^^^^^^ - 43 │ ], - 44 │ }} + 55 │ ], + 56 │ }} i Unsafe fix: Sort the classes. - 40 40 │ "px-2 foo p-4 bar": [ - 41 41 │ "px-2 foo p-4 bar", - 42 │ - → → → {·"px-2·foo·p-4·bar":·"px-2·foo·p-4·bar",·custom:·["px-2·foo·p-4·bar"]·}, - 42 │ + → → → {·"foo·bar·p-4·px-2":·"px-2·foo·p-4·bar",·custom:·["px-2·foo·p-4·bar"]·}, - 43 43 │ ], - 44 44 │ }} + 52 52 │ "px-2 foo p-4 bar": [ + 53 53 │ "px-2 foo p-4 bar", + 54 │ - → → → {·"px-2·foo·p-4·bar":·"px-2·foo·p-4·bar",·custom:·["px-2·foo·p-4·bar"]·}, + 54 │ + → → → {·"foo·bar·p-4·px-2":·"px-2·foo·p-4·bar",·custom:·["px-2·foo·p-4·bar"]·}, + 55 55 │ ], + 56 56 │ }} ``` ``` -unsorted.jsx:42:26 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unsorted.jsx:54:26 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! These CSS classes should be sorted. - 40 │ "px-2 foo p-4 bar": [ - 41 │ "px-2 foo p-4 bar", - > 42 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, + 52 │ "px-2 foo p-4 bar": [ + 53 │ "px-2 foo p-4 bar", + > 54 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, │ ^^^^^^^^^^^^^^^^^^ - 43 │ ], - 44 │ }} + 55 │ ], + 56 │ }} i Unsafe fix: Sort the classes. - 40 40 │ "px-2 foo p-4 bar": [ - 41 41 │ "px-2 foo p-4 bar", - 42 │ - → → → {·"px-2·foo·p-4·bar":·"px-2·foo·p-4·bar",·custom:·["px-2·foo·p-4·bar"]·}, - 42 │ + → → → {·"px-2·foo·p-4·bar":·"foo·bar·p-4·px-2",·custom:·["px-2·foo·p-4·bar"]·}, - 43 43 │ ], - 44 44 │ }} + 52 52 │ "px-2 foo p-4 bar": [ + 53 53 │ "px-2 foo p-4 bar", + 54 │ - → → → {·"px-2·foo·p-4·bar":·"px-2·foo·p-4·bar",·custom:·["px-2·foo·p-4·bar"]·}, + 54 │ + → → → {·"px-2·foo·p-4·bar":·"foo·bar·p-4·px-2",·custom:·["px-2·foo·p-4·bar"]·}, + 55 55 │ ], + 56 56 │ }} ``` ``` -unsorted.jsx:42:55 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unsorted.jsx:54:55 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! These CSS classes should be sorted. - 40 │ "px-2 foo p-4 bar": [ - 41 │ "px-2 foo p-4 bar", - > 42 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, + 52 │ "px-2 foo p-4 bar": [ + 53 │ "px-2 foo p-4 bar", + > 54 │ { "px-2 foo p-4 bar": "px-2 foo p-4 bar", custom: ["px-2 foo p-4 bar"] }, │ ^^^^^^^^^^^^^^^^^^ - 43 │ ], - 44 │ }} + 55 │ ], + 56 │ }} i Unsafe fix: Sort the classes. - 40 40 │ "px-2 foo p-4 bar": [ - 41 41 │ "px-2 foo p-4 bar", - 42 │ - → → → {·"px-2·foo·p-4·bar":·"px-2·foo·p-4·bar",·custom:·["px-2·foo·p-4·bar"]·}, - 42 │ + → → → {·"px-2·foo·p-4·bar":·"px-2·foo·p-4·bar",·custom:·["foo·bar·p-4·px-2"]·}, - 43 43 │ ], - 44 44 │ }} + 52 52 │ "px-2 foo p-4 bar": [ + 53 53 │ "px-2 foo p-4 bar", + 54 │ - → → → {·"px-2·foo·p-4·bar":·"px-2·foo·p-4·bar",·custom:·["px-2·foo·p-4·bar"]·}, + 54 │ + → → → {·"px-2·foo·p-4·bar":·"px-2·foo·p-4·bar",·custom:·["foo·bar·p-4·px-2"]·}, + 55 55 │ ], + 56 56 │ }} ``` diff --git a/packages/tailwindcss-config-analyzer/src/generate-tailwind-preset.ts b/packages/tailwindcss-config-analyzer/src/generate-tailwind-preset.ts index 3dba3d2c6c93..5ca67685d0f9 100644 --- a/packages/tailwindcss-config-analyzer/src/generate-tailwind-preset.ts +++ b/packages/tailwindcss-config-analyzer/src/generate-tailwind-preset.ts @@ -22,11 +22,7 @@ const FILE_HEADER = `//! DO NOT EDIT MANUALLY - this file is autogenerated from use super::sort_config::UtilityLayer; `; -function generateLayer({ layer, classes }: SortConfig["utilities"][number]) { - const header = `const ${layer.toUpperCase()}_LAYER_CLASSES: [&str; ${ - classes.length - }] = [`; - +function generateArrayEntry(header: string, classes: string[]) { // try single line const singleLine = `${header}${classes.map((c) => `"${c}"`).join(", ")}];`; if (singleLine.length < LINE_LIMIT) return `${singleLine}\n`; @@ -37,6 +33,14 @@ function generateLayer({ layer, classes }: SortConfig["utilities"][number]) { .join(",\n")},\n];\n`; } +function generateLayer({ layer, classes }: SortConfig["utilities"][number]) { + const header = `const ${layer.toUpperCase()}_LAYER_CLASSES: [&str; ${ + classes.length + }] = [`; + + return generateArrayEntry(header, classes); +} + function generateLayerArray(layers: SortConfig["utilities"]) { let output = `pub const TAILWIND_LAYERS: [UtilityLayer; ${layers.length}] = [\n`; for (const { layer } of layers) { @@ -49,11 +53,24 @@ function generateLayerArray(layers: SortConfig["utilities"]) { return output; } +// Generate Variants using their name, already ordered by weight +function generateVariants(variants: SortConfig["variants"]) { + let output = `pub const VARIANT_CLASSES: [&str; ${variants.length}] = [\n` + + for (const { name } of variants) { + output += `${INDENT}"${name}",\n` + } + output += `];\n`; + return output; +} + function generateFile(sortConfig: SortConfig) { let output = FILE_HEADER; output += "\n"; output += sortConfig.utilities.map(generateLayer).join("\n"); output += "\n"; + output += generateVariants(sortConfig.variants); + output += "\n"; output += generateLayerArray(sortConfig.utilities); return output; } diff --git a/packages/tailwindcss-config-analyzer/src/introspect.ts b/packages/tailwindcss-config-analyzer/src/introspect.ts index 3b364fa1628c..04fb95ce81d6 100644 --- a/packages/tailwindcss-config-analyzer/src/introspect.ts +++ b/packages/tailwindcss-config-analyzer/src/introspect.ts @@ -41,8 +41,47 @@ function introspectUtilities( return utilities; } +type VariantSpec = { + variant: string; + weight: bigint; +}; + +function introspectVariants(context: TailwindContext): Set { + const variants = new Set(); + // This method returns a list of Variants, each with values but offsets are missing. + const configVariants = context.getVariants(); + // This Map contains weights for each variant name, including those that are values of a main variant. + const variantOffsets = context.offsets.variantOffsets; + + // TODO: Handle isArbitrary like `has-[]` or `group-has-[]` + for (const { name, isArbitrary, values } of configVariants) { + const offset = variantOffsets.get(name); + if (!offset) continue; + + variants.add({ + variant: name, + weight: offset, + }); + + for (const value of values) { + const composedVariantName = `${name}-${value}`; + + const composedVariantOffset = variantOffsets.get(composedVariantName); + if (!composedVariantOffset) continue; + + variants.add({ + variant: composedVariantName, + weight: composedVariantOffset, + }); + } + } + + return variants; +} + export type TailwindSpec = { utilities: Set; + variants: Set; }; export function introspectTailwindConfig( @@ -51,5 +90,6 @@ export function introspectTailwindConfig( ): TailwindSpec { const context = createContextFromConfig(config); const utilities = introspectUtilities(context, options); - return { utilities }; + const variants = introspectVariants(context); + return { utilities, variants }; } diff --git a/packages/tailwindcss-config-analyzer/src/sort-config.ts b/packages/tailwindcss-config-analyzer/src/sort-config.ts index 33bfe59bcfc3..1e872ba8bfe0 100644 --- a/packages/tailwindcss-config-analyzer/src/sort-config.ts +++ b/packages/tailwindcss-config-analyzer/src/sort-config.ts @@ -1,10 +1,16 @@ import type { TailwindSpec, UtilitySpec } from "./introspect.js"; +type Variant = { + name: string; + weight: bigint; +}; + export type SortConfig = { utilities: Array<{ layer: string; classes: Array; }>; + variants: Array; }; function compareBigInt(a: bigint, b: bigint) { @@ -28,6 +34,13 @@ export function sortConfigFromSpec( spec: TailwindSpec, { layerOrder }: { layerOrder: Array }, ): SortConfig { + const utilities = buildConfigUtilities(spec, layerOrder); + const variants = buildConfigVariants(spec); + + return { utilities, variants }; +} + +function buildConfigUtilities(spec: TailwindSpec, layerOrder: Array) { const utilitiesByLayer = new Map>(); for (const utilitySpec of spec.utilities) { const layer = utilitiesByLayer.get(utilitySpec.layer) ?? new Set(); @@ -63,6 +76,13 @@ export function sortConfigFromSpec( classes: [...new Set(classes)], // remove duplicates }; }); + return utilities; +} + +function buildConfigVariants(spec: TailwindSpec): Array { + const variants: Array = [...spec.variants] + .sort((a, b) => compareBigInt(a.weight, b.weight)) + .map((item) => ({ name: item.variant, weight: item.weight })); - return { utilities }; + return variants; } diff --git a/packages/tailwindcss-config-analyzer/src/tailwindcss-module-types.d.ts b/packages/tailwindcss-config-analyzer/src/tailwindcss-module-types.d.ts index d65fd4184ee2..03702b0a9b6c 100644 --- a/packages/tailwindcss-config-analyzer/src/tailwindcss-module-types.d.ts +++ b/packages/tailwindcss-config-analyzer/src/tailwindcss-module-types.d.ts @@ -1,5 +1,5 @@ declare module "tailwindcss/lib/lib/setupContextUtils" { export function createContext( config: ReturnType>, - ): import("./types").TailwindContext; + ): import("./types.ts").TailwindContext; } diff --git a/packages/tailwindcss-config-analyzer/src/types.ts b/packages/tailwindcss-config-analyzer/src/types.ts index 3c27970483d5..451c15abbadd 100644 --- a/packages/tailwindcss-config-analyzer/src/types.ts +++ b/packages/tailwindcss-config-analyzer/src/types.ts @@ -8,6 +8,19 @@ export type CandidateRule = Array< rule: unknown, ] >; + +export type ConfigVariant = { + name: string; + isArbitrary: boolean; + values: string[]; + hasDash: boolean; + selectors: unknown; +}; + export type TailwindContext = { candidateRuleMap: Map; + offsets: { + variantOffsets: Map; + }; + getVariants: () => ConfigVariant[]; };