diff --git a/Cargo.lock b/Cargo.lock index 80676a64..3e9f9ec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,6 +679,7 @@ dependencies = [ name = "extractor" version = "0.1.0" dependencies = [ + "bimap", "boa_engine", "css", "insta", diff --git a/libs/css/src/context_ops.rs b/libs/css/src/context_ops.rs new file mode 100644 index 00000000..0dd3164f --- /dev/null +++ b/libs/css/src/context_ops.rs @@ -0,0 +1,436 @@ +//! Context-aware operations for CSS class name generation. +//! +//! These functions accept an `ExtractionContext` parameter instead of using +//! global state, enabling stateless extraction for parallel processing. +//! +//! Each function has a corresponding global-state version in the parent module +//! for backward compatibility. + +use crate::num_to_nm_base::num_to_nm_base; +use crate::optimize_value::optimize_value; +use bimap::BiHashMap; +use std::collections::HashMap; + +/// Context state for CSS extraction (mirrors ExtractionContext from extractor crate) +/// +/// This is a simplified view of the state needed by css crate functions. +/// The extractor crate's ExtractionContext wraps this. +#[derive(Debug, Default)] +pub struct CssContext { + /// Maps filename → (style_key → class_number) + pub class_map: HashMap>, + /// Bidirectional map: filename ↔ file_number + pub file_map: BiHashMap, + /// Optional prefix for all generated class names + pub prefix: Option, + /// Debug mode flag + pub debug: bool, +} + +impl CssContext { + /// Creates a new empty context. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Gets the file number for a filename, creating a new mapping if needed. + pub fn get_file_num(&mut self, filename: &str) -> usize { + let len = self.file_map.len(); + if !self.file_map.contains_left(filename) { + self.file_map.insert(filename.to_string(), len); + } + *self.file_map.get_by_left(filename).unwrap() + } + + /// Gets the filename for a file number. + #[must_use] + pub fn get_filename(&self, file_num: usize) -> Option<&str> { + self.file_map.get_by_right(&file_num).map(String::as_str) + } + + /// Resets all state. + pub fn reset(&mut self) { + self.class_map.clear(); + self.file_map.clear(); + } +} + +fn encode_selector(selector: &str) -> String { + let mut result = String::with_capacity(selector.len() * 2); + for c in selector.chars() { + match c { + '&' => result.push_str("_a_"), + ':' => result.push_str("_c_"), + '(' => result.push_str("_lp_"), + ')' => result.push_str("_rp_"), + '[' => result.push_str("_lb_"), + ']' => result.push_str("_rb_"), + '=' => result.push_str("_eq_"), + '>' => result.push_str("_gt_"), + '<' => result.push_str("_lt_"), + '~' => result.push_str("_tl_"), + '+' => result.push_str("_pl_"), + ' ' => result.push_str("_s_"), + '*' => result.push_str("_st_"), + '.' => result.push_str("_d_"), + '#' => result.push_str("_h_"), + ',' => result.push_str("_cm_"), + '"' => result.push_str("_dq_"), + '\'' => result.push_str("_sq_"), + '/' => result.push_str("_sl_"), + '\\' => result.push_str("_bs_"), + '%' => result.push_str("_pc_"), + '^' => result.push_str("_cr_"), + '$' => result.push_str("_dl_"), + '|' => result.push_str("_pp_"), + '@' => result.push_str("_at_"), + '!' => result.push_str("_ex_"), + '?' => result.push_str("_qm_"), + ';' => result.push_str("_sc_"), + '{' => result.push_str("_lc_"), + '}' => result.push_str("_rc_"), + '-' => result.push('-'), + '_' => result.push('_'), + _ if c.is_ascii_alphanumeric() => result.push(c), + _ => { + result.push_str("_u"); + result.push_str(&format!("{:04x}", c as u32)); + result.push('_'); + } + } + } + result +} + +/// Generate a class name for a style sheet entry (context-aware version). +/// +/// This is the context-aware equivalent of `sheet_to_classname`. +pub fn sheet_to_classname_with_context( + ctx: &mut CssContext, + property: &str, + level: u8, + value: Option<&str>, + selector: Option<&str>, + style_order: Option, + filename: Option<&str>, +) -> String { + // Copy prefix to avoid borrow issues + let prefix = ctx.prefix.clone().unwrap_or_default(); + let debug = ctx.debug; + + // base style + let filename = if style_order == Some(0) { + None + } else { + filename + }; + if debug { + let selector = selector.unwrap_or_default().trim(); + let file_suffix = filename + .map(|v| format!("-{}", ctx.get_file_num(v))) + .unwrap_or_default(); + format!( + "{}{}-{}-{}-{}-{}{}", + prefix, + property.trim(), + level, + optimize_value(value.unwrap_or_default()), + if selector.is_empty() { + String::new() + } else { + encode_selector(selector) + }, + style_order.unwrap_or(255), + file_suffix, + ) + } else { + let file_num_suffix = filename + .map(|v| format!("-{}", ctx.get_file_num(v))) + .unwrap_or_default(); + let key = format!( + "{}-{}-{}-{}-{}{}", + property.trim(), + level, + optimize_value(value.unwrap_or_default()), + selector.unwrap_or_default().trim(), + style_order.unwrap_or(255), + file_num_suffix, + ); + let filename_str = filename.map(|v| v.to_string()).unwrap_or_default(); + let file_num = if !filename_str.is_empty() { + Some(ctx.get_file_num(&filename_str)) + } else { + None + }; + let file_entry = ctx.class_map.entry(filename_str).or_default(); + let class_num = if let Some(&num) = file_entry.get(&key) { + num_to_nm_base(num) + } else { + let len = file_entry.len(); + file_entry.insert(key, len); + num_to_nm_base(len) + }; + if let Some(fnum) = file_num { + format!("{}{}-{}", prefix, num_to_nm_base(fnum), class_num) + } else { + format!("{}{}", prefix, class_num) + } + } +} + +/// Generate a CSS variable name (context-aware version). +/// +/// This is the context-aware equivalent of `sheet_to_variable_name`. +pub fn sheet_to_variable_name_with_context( + ctx: &mut CssContext, + property: &str, + level: u8, + selector: Option<&str>, +) -> String { + // Copy prefix to avoid borrow issues + let prefix = ctx.prefix.clone().unwrap_or_default(); + let debug = ctx.debug; + + if debug { + let selector = selector.unwrap_or_default().trim(); + format!( + "--{}{}-{}-{}", + prefix, + property, + level, + if selector.is_empty() { + String::new() + } else { + encode_selector(selector) + } + ) + } else { + let key = format!( + "{}-{}-{}", + property, + level, + selector.unwrap_or_default().trim() + ); + let file_entry = ctx.class_map.entry(String::new()).or_default(); + if let Some(&num) = file_entry.get(&key) { + format!("--{}{}", prefix, num_to_nm_base(num)) + } else { + let len = file_entry.len(); + file_entry.insert(key, len); + format!("--{}{}", prefix, num_to_nm_base(len)) + } + } +} + +/// Generate a keyframes animation name (context-aware version). +/// +/// This is the context-aware equivalent of `keyframes_to_keyframes_name`. +pub fn keyframes_to_keyframes_name_with_context( + ctx: &mut CssContext, + keyframes: &str, + filename: Option<&str>, +) -> String { + // Copy prefix to avoid borrow issues + let prefix = ctx.prefix.clone().unwrap_or_default(); + let debug = ctx.debug; + + if debug { + format!("{}k-{keyframes}", prefix) + } else { + let key = format!("k-{keyframes}"); + let filename_str = filename.map(|v| v.to_string()).unwrap_or_default(); + let file_num = if !filename_str.is_empty() { + Some(ctx.get_file_num(&filename_str)) + } else { + None + }; + let file_entry = ctx.class_map.entry(filename_str).or_default(); + let class_num = if let Some(&num) = file_entry.get(&key) { + num_to_nm_base(num).to_string() + } else { + let len = file_entry.len(); + file_entry.insert(key, len); + num_to_nm_base(len).to_string() + }; + if let Some(fnum) = file_num { + format!("{}{}-{}", prefix, num_to_nm_base(fnum), class_num) + } else { + format!("{}{}", prefix, class_num) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sheet_to_classname_basic() { + let mut ctx = CssContext::new(); + ctx.debug = false; + + assert_eq!( + sheet_to_classname_with_context( + &mut ctx, + "background", + 0, + Some("red"), + None, + None, + None + ), + "a" + ); + assert_eq!( + sheet_to_classname_with_context(&mut ctx, "color", 0, Some("blue"), None, None, None), + "b" + ); + // Same style returns same class + assert_eq!( + sheet_to_classname_with_context( + &mut ctx, + "background", + 0, + Some("red"), + None, + None, + None + ), + "a" + ); + } + + #[test] + fn test_sheet_to_classname_debug() { + let mut ctx = CssContext::new(); + ctx.debug = true; + + assert_eq!( + sheet_to_classname_with_context( + &mut ctx, + "background", + 0, + Some("red"), + None, + None, + None + ), + "background-0-red--255" + ); + } + + #[test] + fn test_sheet_to_classname_with_prefix() { + let mut ctx = CssContext::new(); + ctx.debug = false; + ctx.prefix = Some("app-".to_string()); + + assert_eq!( + sheet_to_classname_with_context( + &mut ctx, + "background", + 0, + Some("red"), + None, + None, + None + ), + "app-a" + ); + } + + #[test] + fn test_sheet_to_classname_with_filename() { + let mut ctx = CssContext::new(); + ctx.debug = false; + + let class1 = sheet_to_classname_with_context( + &mut ctx, + "background", + 0, + Some("red"), + None, + None, + Some("file1.tsx"), + ); + let class2 = sheet_to_classname_with_context( + &mut ctx, + "background", + 0, + Some("red"), + None, + None, + Some("file2.tsx"), + ); + // Different files should have different prefixes + assert_ne!(class1, class2); + } + + #[test] + fn test_sheet_to_variable_name_basic() { + let mut ctx = CssContext::new(); + ctx.debug = false; + + assert_eq!( + sheet_to_variable_name_with_context(&mut ctx, "background", 0, None), + "--a" + ); + assert_eq!( + sheet_to_variable_name_with_context(&mut ctx, "color", 0, None), + "--b" + ); + } + + #[test] + fn test_sheet_to_variable_name_debug() { + let mut ctx = CssContext::new(); + ctx.debug = true; + + assert_eq!( + sheet_to_variable_name_with_context(&mut ctx, "background", 0, None), + "--background-0-" + ); + } + + #[test] + fn test_keyframes_basic() { + let mut ctx = CssContext::new(); + ctx.debug = false; + + assert_eq!( + keyframes_to_keyframes_name_with_context(&mut ctx, "spin", None), + "a" + ); + assert_eq!( + keyframes_to_keyframes_name_with_context(&mut ctx, "spin", None), + "a" + ); + assert_eq!( + keyframes_to_keyframes_name_with_context(&mut ctx, "fade", None), + "b" + ); + } + + #[test] + fn test_keyframes_debug() { + let mut ctx = CssContext::new(); + ctx.debug = true; + + assert_eq!( + keyframes_to_keyframes_name_with_context(&mut ctx, "spin", None), + "k-spin" + ); + } + + #[test] + fn test_keyframes_with_filename() { + let mut ctx = CssContext::new(); + ctx.debug = false; + + let name1 = keyframes_to_keyframes_name_with_context(&mut ctx, "spin", Some("file1.tsx")); + let name2 = keyframes_to_keyframes_name_with_context(&mut ctx, "spin", Some("file2.tsx")); + // Different files should have different prefixes + assert_ne!(name1, name2); + } +} diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index 456cd4bd..ad6b0de3 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -1,5 +1,6 @@ pub mod class_map; mod constant; +pub mod context_ops; pub mod debug; pub mod file_map; pub mod is_special_property; diff --git a/libs/extractor/Cargo.toml b/libs/extractor/Cargo.toml index cc4936df..f183ae4f 100644 --- a/libs/extractor/Cargo.toml +++ b/libs/extractor/Cargo.toml @@ -19,6 +19,7 @@ strum = "0.27.2" strum_macros = "0.27.2" serde_json = "1.0" boa_engine = "0.21" +bimap = "0.6.3" [dev-dependencies] insta = "1.46.3" diff --git a/libs/extractor/src/context.rs b/libs/extractor/src/context.rs new file mode 100644 index 00000000..cc864b5a --- /dev/null +++ b/libs/extractor/src/context.rs @@ -0,0 +1,320 @@ +//! Extraction context for stateless CSS extraction. +//! +//! This module provides `ExtractionContext` which holds all mutable state +//! needed during CSS extraction. By passing context explicitly rather than +//! using global state, we enable parallel file processing for Turbopack +//! multi-core builds. +//! +//! # Usage +//! +//! ```rust,ignore +//! use extractor::{ExtractionContext, extract_with_context, ExtractOption}; +//! +//! // Create a shared context for the build +//! let mut ctx = ExtractionContext::new(); +//! +//! // Extract styles from multiple files (can be parallelized) +//! let result1 = extract_with_context("file1.tsx", code1, options, &mut ctx)?; +//! let result2 = extract_with_context("file2.tsx", code2, options, &mut ctx)?; +//! ``` + +use bimap::BiHashMap; +use std::collections::HashMap; + +/// Extraction context holding all mutable state for CSS extraction. +/// +/// This struct replaces the global `GLOBAL_CLASS_MAP`, `GLOBAL_FILE_MAP`, +/// and `GLOBAL_PREFIX` statics, enabling stateless extraction that can +/// be parallelized across multiple files. +/// +/// # Thread Safety +/// +/// The context itself is NOT thread-safe. For parallel extraction: +/// - Use separate contexts per thread, then merge +/// - Or wrap in `Arc>` if shared access needed +/// +/// The WASM boundary maintains a single global context for backward +/// compatibility with the existing JS API. +#[derive(Debug, Default)] +pub struct ExtractionContext { + /// Maps filename → (style_key → class_number) + /// + /// Used for generating unique, deterministic class names. + /// The outer key is the filename (empty string for global styles). + /// The inner map tracks which style combinations have been seen + /// and assigns sequential numbers for minimal class names. + pub(crate) class_map: HashMap>, + + /// Bidirectional map: filename ↔ file_number + /// + /// Used for per-file class name prefixes to avoid collisions + /// between files while keeping class names short. + pub(crate) file_map: BiHashMap, + + /// Optional prefix for all generated class names. + /// + /// When set, all class names will be prefixed with this string. + /// Example: prefix "app-" → class names become "app-a", "app-b", etc. + pub(crate) prefix: Option, + + /// Debug mode flag. + /// + /// When true, generates human-readable class names for debugging. + /// Example: "background-0-red-hover-255" instead of "a" + pub(crate) debug: bool, +} + +impl ExtractionContext { + /// Creates a new empty extraction context. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Creates a context with a specific prefix. + #[must_use] + pub fn with_prefix(prefix: Option) -> Self { + Self { + prefix, + ..Default::default() + } + } + + /// Sets the class name prefix. + pub fn set_prefix(&mut self, prefix: Option) { + self.prefix = prefix; + } + + /// Gets the current prefix. + #[must_use] + pub fn get_prefix(&self) -> Option<&str> { + self.prefix.as_deref() + } + + /// Sets debug mode. + pub fn set_debug(&mut self, debug: bool) { + self.debug = debug; + } + + /// Gets debug mode. + #[must_use] + pub fn is_debug(&self) -> bool { + self.debug + } + + /// Resets the class map (for testing). + pub fn reset_class_map(&mut self) { + self.class_map.clear(); + } + + /// Resets the file map (for testing). + pub fn reset_file_map(&mut self) { + self.file_map.clear(); + } + + /// Resets all state (for testing). + pub fn reset(&mut self) { + self.class_map.clear(); + self.file_map.clear(); + } + + /// Gets the file number for a filename, creating a new mapping if needed. + pub fn get_file_num(&mut self, filename: &str) -> usize { + let len = self.file_map.len(); + if !self.file_map.contains_left(filename) { + self.file_map.insert(filename.to_string(), len); + } + *self.file_map.get_by_left(filename).unwrap() + } + + /// Gets the filename for a file number. + #[must_use] + pub fn get_filename(&self, file_num: usize) -> Option<&str> { + self.file_map.get_by_right(&file_num).map(String::as_str) + } + + /// Sets the class map from external data (for WASM interop). + pub fn set_class_map(&mut self, map: HashMap>) { + self.class_map = map; + } + + /// Gets a clone of the class map (for WASM interop). + #[must_use] + pub fn get_class_map(&self) -> HashMap> { + self.class_map.clone() + } + + /// Sets the file map from external data (for WASM interop). + pub fn set_file_map(&mut self, map: BiHashMap) { + self.file_map = map; + } + + /// Gets a clone of the file map (for WASM interop). + #[must_use] + pub fn get_file_map(&self) -> BiHashMap { + self.file_map.clone() + } + + /// Gets or creates a class number for a style key in a file. + /// + /// This is the core method for generating deterministic class names. + /// Given a style key (property-value-selector combination), it returns + /// a unique number that can be converted to a short class name. + pub fn get_or_create_class_num(&mut self, filename: &str, style_key: &str) -> usize { + let file_map = self.class_map.entry(filename.to_string()).or_default(); + if let Some(&num) = file_map.get(style_key) { + num + } else { + let num = file_map.len(); + file_map.insert(style_key.to_string(), num); + num + } + } + + /// Gets an existing class number without creating a new one. + #[must_use] + pub fn get_class_num(&self, filename: &str, style_key: &str) -> Option { + self.class_map + .get(filename) + .and_then(|m| m.get(style_key).copied()) + } +} + +/// Synchronize context with global state (for incremental migration). +/// +/// This allows using ExtractionContext while the codebase still uses global +/// state internally. Once the full migration is complete, this can be removed. +impl ExtractionContext { + /// Populate global state from this context. + /// + /// Call this before extraction to set up the global state. + pub fn sync_to_globals(&self) { + css::class_map::set_class_map(self.class_map.clone()); + css::file_map::set_file_map(self.file_map.clone()); + css::set_prefix(self.prefix.clone()); + css::debug::set_debug(self.debug); + } + + /// Populate this context from global state. + /// + /// Call this after extraction to capture any state changes. + pub fn sync_from_globals(&mut self) { + self.class_map = css::class_map::get_class_map(); + self.file_map = css::file_map::get_file_map(); + self.prefix = css::get_prefix(); + self.debug = css::debug::is_debug(); + } + + /// Create a context initialized from current global state. + #[must_use] + pub fn from_globals() -> Self { + Self { + class_map: css::class_map::get_class_map(), + file_map: css::file_map::get_file_map(), + prefix: css::get_prefix(), + debug: css::debug::is_debug(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_context() { + let ctx = ExtractionContext::new(); + assert!(ctx.class_map.is_empty()); + assert!(ctx.file_map.is_empty()); + assert!(ctx.prefix.is_none()); + assert!(!ctx.debug); + } + + #[test] + fn test_with_prefix() { + let ctx = ExtractionContext::with_prefix(Some("app-".to_string())); + assert_eq!(ctx.get_prefix(), Some("app-")); + } + + #[test] + fn test_set_prefix() { + let mut ctx = ExtractionContext::new(); + ctx.set_prefix(Some("test-".to_string())); + assert_eq!(ctx.get_prefix(), Some("test-")); + ctx.set_prefix(None); + assert_eq!(ctx.get_prefix(), None); + } + + #[test] + fn test_debug_mode() { + let mut ctx = ExtractionContext::new(); + assert!(!ctx.is_debug()); + ctx.set_debug(true); + assert!(ctx.is_debug()); + } + + #[test] + fn test_file_num_mapping() { + let mut ctx = ExtractionContext::new(); + + // First file gets 0 + assert_eq!(ctx.get_file_num("file1.tsx"), 0); + // Same file returns same number + assert_eq!(ctx.get_file_num("file1.tsx"), 0); + // Second file gets 1 + assert_eq!(ctx.get_file_num("file2.tsx"), 1); + // Lookup by number + assert_eq!(ctx.get_filename(0), Some("file1.tsx")); + assert_eq!(ctx.get_filename(1), Some("file2.tsx")); + assert_eq!(ctx.get_filename(99), None); + } + + #[test] + fn test_class_num_generation() { + let mut ctx = ExtractionContext::new(); + + // First style in file gets 0 + assert_eq!(ctx.get_or_create_class_num("", "bg-red"), 0); + // Same style returns same number + assert_eq!(ctx.get_or_create_class_num("", "bg-red"), 0); + // Different style gets next number + assert_eq!(ctx.get_or_create_class_num("", "color-blue"), 1); + // Different file has its own numbering + assert_eq!(ctx.get_or_create_class_num("file.tsx", "bg-red"), 0); + + // Lookup without creation + assert_eq!(ctx.get_class_num("", "bg-red"), Some(0)); + assert_eq!(ctx.get_class_num("", "unknown"), None); + } + + #[test] + fn test_reset() { + let mut ctx = ExtractionContext::new(); + ctx.get_file_num("file.tsx"); + ctx.get_or_create_class_num("", "style"); + + ctx.reset(); + + assert!(ctx.class_map.is_empty()); + assert!(ctx.file_map.is_empty()); + } + + #[test] + fn test_set_and_get_maps() { + let mut ctx = ExtractionContext::new(); + + // Set class map + let mut class_map = HashMap::new(); + let mut inner = HashMap::new(); + inner.insert("key".to_string(), 42); + class_map.insert("file".to_string(), inner); + ctx.set_class_map(class_map.clone()); + assert_eq!(ctx.get_class_map(), class_map); + + // Set file map + let mut file_map = BiHashMap::new(); + file_map.insert("test.tsx".to_string(), 5); + ctx.set_file_map(file_map.clone()); + assert_eq!(ctx.get_file_map(), file_map); + } +} diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index a27c6177..dfe2a9b2 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -1,5 +1,6 @@ mod as_visit; mod component; +pub mod context; mod css_utils; pub mod extract_style; mod extractor; @@ -12,7 +13,8 @@ mod util_type; mod utils; mod vanilla_extract; mod visit; -use crate::extract_style::extract_style_value::ExtractStyleValue; +pub use crate::context::ExtractionContext; +pub use crate::extract_style::extract_style_value::ExtractStyleValue; use crate::visit::DevupVisitor; use css::file_map::get_file_num_by_filename; use oxc_allocator::Allocator; @@ -266,9 +268,53 @@ pub fn extract( }) } +/// Extract styles from code using an explicit context instead of global state. +/// +/// This function enables stateless extraction for parallel processing (e.g., Turbopack). +/// It synchronizes the context with global state before extraction and captures any +/// state changes afterward. +/// +/// # Arguments +/// +/// * `filename` - The source file path (used for file-specific class prefixes) +/// * `code` - The source code to extract styles from +/// * `option` - Extraction options (package name, CSS directory, etc.) +/// * `ctx` - Mutable reference to the extraction context +/// +/// # Example +/// +/// ```rust,ignore +/// use extractor::{ExtractionContext, extract_with_context, ExtractOption}; +/// +/// let mut ctx = ExtractionContext::new(); +/// let result = extract_with_context( +/// "component.tsx", +/// r#"import { Box } from '@devup-ui/react'; export default () => "#, +/// ExtractOption::default(), +/// &mut ctx, +/// )?; +/// ``` +pub fn extract_with_context( + filename: &str, + code: &str, + option: ExtractOption, + ctx: &mut ExtractionContext, +) -> Result> { + // Sync context state to globals before extraction + ctx.sync_to_globals(); + + // Perform extraction using existing implementation + let result = extract(filename, code, option); + + // Capture any state changes back to context + ctx.sync_from_globals(); + + result +} + /// Extract class names from generated code for specific style names /// Used for two-pass vanilla-extract processing to resolve selector references -fn extract_class_map_from_code( +pub fn extract_class_map_from_code( filename: &str, partial_code: &str, option: &ExtractOption, @@ -372,13129 +418,3 @@ pub fn has_devup_ui(filename: &str, code: &str, package: &str) -> bool { false } - -#[cfg(test)] -mod tests { - use std::collections::BTreeSet; - - use super::*; - use css::class_map::reset_class_map; - use css::file_map::reset_file_map; - use insta::assert_debug_snapshot; - use rstest::rstest; - use serial_test::serial; - - #[derive(Debug)] - #[allow(dead_code)] - struct ToBTreeSet { - // used styles - pub(crate) styles: BTreeSet, - - // output source - pub(crate) code: String, - } - - impl From for ToBTreeSet { - fn from(output: ExtractOutput) -> Self { - Self { - styles: { - let mut set = BTreeSet::new(); - set.extend(output.styles); - set - }, - code: output.code, - } - } - } - - #[test] - fn test_extract_option_default() { - // Tests lines 90-91: ExtractOption::default() - let option = ExtractOption::default(); - assert_eq!(option.package, "@devup-ui/react"); - assert_eq!(option.css_dir, "@devup-ui/react"); - assert!(!option.single_css); - assert!(!option.import_main_css); - assert!(option.import_aliases.is_empty()); - } - - #[test] - #[serial] - fn extract_just_tsx() { - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - "const a = 1;", - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - }, - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - "", - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - }, - ) - .unwrap() - )); - } - #[test] - #[serial] - fn ignore_special_props() { - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - {}} aria-valuenow={24} key={2} tabIndex={1} id="id" /> - "#, - ExtractOption { package: "@devup-ui/core".to_string(), css_dir: "@devup-ui/core".to_string(), single_css: true, import_main_css: false, import_aliases: HashMap::new() } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Input} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - } - - #[test] - #[serial] - fn convert_tag() { - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { package: "@devup-ui/core".to_string(), css_dir: "@devup-ui/core".to_string(), single_css: true, import_main_css: false, import_aliases: HashMap::new() } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { package: "@devup-ui/core".to_string(), css_dir: "@devup-ui/core".to_string(), single_css: true, import_main_css: false, import_aliases: HashMap::new() } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - // maintain object expression - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - // maintain object expression - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r#"import {Box} from '@devup-ui/core' - - "#, - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - } - #[test] - #[serial] - fn extract_style_props() { - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r"import {Box} from '@devup-ui/core' - /> - ", - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r"import {Box as C} from '@devup-ui/core' - - ", - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r"import {Input} from '@devup-ui/core' - - ", - ExtractOption { - package: "@devup-ui/core".to_string(), - css_dir: "@devup-ui/core".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - } - ) - .unwrap() - )); - - reset_class_map(); - reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r"import {Button} from '@devup-ui/core' -