diff --git a/.changepacks/changepack_log_YZCbpfsBZLpjLRVNGY_h7.json b/.changepacks/changepack_log_YZCbpfsBZLpjLRVNGY_h7.json new file mode 100644 index 00000000..d7f40473 --- /dev/null +++ b/.changepacks/changepack_log_YZCbpfsBZLpjLRVNGY_h7.json @@ -0,0 +1 @@ +{"changes":{"packages/plugin-utils/package.json":"Patch","bindings/devup-ui-wasm/package.json":"Patch","packages/webpack-plugin/package.json":"Patch","packages/components/package.json":"Patch","packages/rsbuild-plugin/package.json":"Patch","packages/vite-plugin/package.json":"Patch","packages/next-plugin/package.json":"Patch","packages/bun-plugin/package.json":"Patch","packages/reset-css/package.json":"Patch","packages/react/package.json":"Patch","packages/eslint-plugin/package.json":"Patch"},"note":"Support conditonal styleOrder","date":"2026-02-10T07:40:05.477045100Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8ed2c16f..023792ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1070,9 +1070,9 @@ checksum = "a3c2a6c0b4b5637c41719973ef40c6a1cf564f9db6958350de6193fbee9c23f5" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libm" @@ -1109,9 +1109,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -1882,9 +1882,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1894,9 +1894,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1905,9 +1905,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "regress" @@ -2461,9 +2461,9 @@ checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-linebreak" @@ -2880,6 +2880,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/libs/css/Cargo.toml b/libs/css/Cargo.toml index 028d261f..8efaad40 100644 --- a/libs/css/Cargo.toml +++ b/libs/css/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" once_cell = "1.21.3" phf = { version = "0.13", features = ["macros"] } serde = { version = "1.0.228", features = ["derive"] } -regex = "1.12.2" +regex = "1.12.3" bimap = { version = "0.6.3" } [dev-dependencies] diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index a27c6177..638883a8 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -15,7 +15,7 @@ mod visit; 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; +use oxc_allocator::{Allocator, CloneIn}; use oxc_ast::ast::Expression; use oxc_ast_visit::VisitMut; use oxc_codegen::{Codegen, CodegenOptions}; @@ -57,7 +57,45 @@ pub enum ExtractStyleProp<'a> { }, } -impl ExtractStyleProp<'_> { +impl<'a> ExtractStyleProp<'a> { + pub fn clone_in(&self, alloc: &'a Allocator) -> Self { + match self { + ExtractStyleProp::Static(v) => ExtractStyleProp::Static(v.clone()), + ExtractStyleProp::StaticArray(arr) => { + ExtractStyleProp::StaticArray(arr.iter().map(|s| s.clone_in(alloc)).collect()) + } + ExtractStyleProp::Conditional { + condition, + consequent, + alternate, + } => ExtractStyleProp::Conditional { + condition: condition.clone_in(alloc), + consequent: consequent.as_ref().map(|c| Box::new(c.clone_in(alloc))), + alternate: alternate.as_ref().map(|a| Box::new(a.clone_in(alloc))), + }, + ExtractStyleProp::Enum { condition, map } => ExtractStyleProp::Enum { + condition: condition.clone_in(alloc), + map: map + .iter() + .map(|(k, v)| (k.clone(), v.iter().map(|s| s.clone_in(alloc)).collect())) + .collect(), + }, + ExtractStyleProp::Expression { styles, expression } => ExtractStyleProp::Expression { + styles: styles.clone(), + expression: expression.clone_in(alloc), + }, + ExtractStyleProp::MemberExpression { map, expression } => { + ExtractStyleProp::MemberExpression { + map: map + .iter() + .map(|(k, v)| (k.clone(), Box::new(v.clone_in(alloc)))) + .collect(), + expression: expression.clone_in(alloc), + } + } + } + } + pub fn extract(&self) -> Vec { match self { ExtractStyleProp::Static(style) => vec![style.clone()], @@ -6202,6 +6240,409 @@ export { )); } + #[test] + #[serial] + fn style_order_conditional() { + // Test 1: styleOrder={condition ? 5 : 10} — ternary with two static numbers + // Since jsx_expression_to_number doesn't handle ConditionalExpression, + // styleOrder should be None (not applied) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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 2: styleOrder={condition ? 5 : variable} — ternary with mixed static/dynamic + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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 3: styleOrder={variable} — fully dynamic styleOrder + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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 4: styleOrder={condition ? 5 : 10} with conditional style props + // Verifies interaction between conditional styleOrder and conditional style values + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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 5: styleOrder={condition ? 5 : 10} with css() className + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + r#"import {Box, css} 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 style_order_logical_and() { + // Test 1: JSX path — styleOrder={a === 1 && 5} + // truthy → styleOrder=5, falsy → no styleOrder (None) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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 2: JSX path — styleOrder={a === 1 && 5} with conditional style props + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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 3: Call expression path — styleOrder: a === 1 && 5 + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, { styleOrder: a === 1 && 5, bg: "red", p: "4" }); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + } + + #[test] + #[serial] + fn style_order_conditional_coverage() { + // Coverage: lib.rs:53 — Enum variant clone_in (positioning={variable} with conditional styleOrder) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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() + )); + + // Coverage: lib.rs:54 — Expression variant clone_in (typography={variable} with conditional styleOrder) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + r#"import {Text} 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() + )); + + // Coverage: lib.rs:55 — MemberExpression variant clone_in (computed member expression with conditional styleOrder) + // bg={["red","blue"][idx]} produces ExtractStyleProp::MemberExpression which gets clone_in'd + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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() + )); + + // Coverage: prop_modify_utils.rs:58,130 — (None, None) branch: no styles, only conditional styleOrder + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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() + )); + + // Coverage: prop_modify_utils.rs:56-58 — (None, None) branch via call expression path + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, { styleOrder: isActive ? 5 : 10 }); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Coverage: utils.rs:100 — expression_to_style_order with plain variable (not conditional, not static) + // Also covers visit.rs:259,262 — fallback from pre-scan None to extract_style_from_expression's result + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, { styleOrder: order, bg: "red" }); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Coverage: visit.rs:297/492 — non-conditional static styleOrder via call expression (fallback _ branch) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, { bg: "red" }); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + } + + #[test] + #[serial] + fn style_order_conditional_call_expression() { + // Test 1: styleOrder: isActive ? 5 : 10 — ternary with two static numbers via call expression + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, { styleOrder: isActive ? 5 : 10, bg: "red", p: "4" }); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Test 2: styleOrder: isActive ? 5 : 10 with conditional style props via call expression + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, { styleOrder: isActive ? 5 : 10, bg: isActive ? "red" : "blue" }); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Test 3: static styleOrder via call expression (backward compat) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, { styleOrder: 5, bg: "red", p: "4" }); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + } + #[test] #[serial] fn style_order2() { @@ -13497,4 +13938,194 @@ export const Card = () => ( .unwrap() )); } + + #[test] + #[serial] + fn style_order_coverage_additional() { + // Coverage: visit.rs:241 — non-object 2nd argument in call expression + // When 2nd arg is a variable (not ObjectExpression), pre-scan should return None + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, someVariable); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Coverage: visit.rs:492 — non-conditional _ branch in JSX with static styleOrder + // Ensures the `_ =>` (non-Conditional) match arm in visit_jsx_element is hit + // with an explicit static styleOrder value + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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() + )); + + // Coverage: visit.rs:259,262 — ParsedStyleOrder::None fallback with static styleOrder + // via call expression where pre-scan finds styleOrder=5 (static), so extract_style_from_expression + // also returns style_order=Some(5), but the pre-scan already resolved it to Static(5) + // This ensures the full `ParsedStyleOrder::None => match style_order` path in visit_call_expression + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, { styleOrder: 5, bg: "red", p: 4 }); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Coverage: lib.rs:55 — MemberExpression variant clone_in with conditional styleOrder + // Requires a computed member expression pattern (array/object indexed by variable) + // to produce ExtractStyleProp::MemberExpression, which is then cloned for the alternate branch + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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() + )); + + // Coverage: visit.rs — ParsedStyleOrder::None + _ match arm in visit_call_expression + // Call expression WITHOUT any styleOrder property. + // Pre-scan finds no styleOrder → ParsedStyleOrder::None. + // extract_style_from_expression also returns style_order=None. + // Exercises: ParsedStyleOrder::None branch and the _ => (non-Conditional) arm. + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.mjs", + r#"import { jsx as e } from "react/jsx-runtime"; +import { Box as o } from "@devup-ui/react"; +function c() { + return e(o, { bg: "red", p: 4 }); +} +export { c as Lib };"#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Coverage: visit.rs — _ match arm in visit_jsx_element (non-Conditional path) + // JSX element WITHOUT any styleOrder prop at all. + // parsed_style_order stays ParsedStyleOrder::None (default). + // Exercises the _ => arm in visit_jsx_element's match on parsed_style_order. + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.jsx", + 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] + fn test_combine_conditional_class_name() { + use crate::prop_modify_utils::combine_conditional_class_name; + use crate::utils::expression_to_code; + use oxc_span::SPAN; + + let allocator = Allocator::default(); + let builder = oxc_ast::AstBuilder::new(&allocator); + + let make_cond = || builder.expression_identifier(SPAN, builder.atom("cond")); + let make_str = |s| builder.expression_string_literal(SPAN, builder.atom(s), None); + + // (Some, Some) — both branches have classNames + let result = combine_conditional_class_name( + &builder, + make_cond(), + Some(make_str("a")), + Some(make_str("b")), + ); + assert!(result.is_some()); + let code = expression_to_code(&result.unwrap()); + assert!(code.contains("cond"), "expected condition in: {code}"); + + // (Some, None) — only consequent has className, alternate falls back to "" + let result = + combine_conditional_class_name(&builder, make_cond(), Some(make_str("a")), None); + assert!(result.is_some()); + let code = expression_to_code(&result.unwrap()); + assert!(code.contains("cond"), "expected condition in: {code}"); + + // (None, Some) — only alternate has className, consequent falls back to "" + let result = + combine_conditional_class_name(&builder, make_cond(), None, Some(make_str("b"))); + assert!(result.is_some()); + let code = expression_to_code(&result.unwrap()); + assert!(code.contains("cond"), "expected condition in: {code}"); + + // (None, None) — neither has className + let result = combine_conditional_class_name(&builder, make_cond(), None, None); + assert!(result.is_none()); + } } diff --git a/libs/extractor/src/prop_modify_utils.rs b/libs/extractor/src/prop_modify_utils.rs index 83b58442..c2394f88 100644 --- a/libs/extractor/src/prop_modify_utils.rs +++ b/libs/extractor/src/prop_modify_utils.rs @@ -15,8 +15,40 @@ use oxc_ast::ast::{ use oxc_span::SPAN; use std::collections::HashMap; +/// Combine two optional className expressions into a conditional expression. +/// `condition ? con_expr : alt_expr`, falling back to `""` for the missing branch. +/// Returns `None` only when both branches are `None`. +pub(crate) fn combine_conditional_class_name<'a>( + ast_builder: &AstBuilder<'a>, + condition: Expression<'a>, + con_expr: Option>, + alt_expr: Option>, +) -> Option> { + match (con_expr, alt_expr) { + (Some(con), Some(alt)) => { + Some(ast_builder.expression_conditional(SPAN, condition, con, alt)) + } + (Some(con), None) => Some(ast_builder.expression_conditional( + SPAN, + condition, + con, + ast_builder.expression_string_literal(SPAN, "", None), + )), + (None, Some(alt)) => Some(ast_builder.expression_conditional( + SPAN, + condition, + ast_builder.expression_string_literal(SPAN, "", None), + alt, + )), + (None, None) => None, + } +} + /// modify object props /// Returns extracted Tailwind styles from static className strings +/// `conditional_branch`: If Some, contains (condition, alternate_styles, alternate_style_order) +/// for generating a conditional className expression: `condition ? consequent_class : alternate_class` +#[allow(clippy::too_many_arguments)] pub fn modify_prop_object<'a>( ast_builder: &AstBuilder<'a>, props: &mut oxc_allocator::Vec>, @@ -25,6 +57,7 @@ pub fn modify_prop_object<'a>( style_vars: Option>, props_prop: Option>, filename: Option<&str>, + conditional_branch: Option<(Expression<'a>, &mut [ExtractStyleProp<'a>], Option)>, ) -> Vec { let mut class_name_prop = None; let mut style_prop = None; @@ -52,14 +85,46 @@ pub fn modify_prop_object<'a>( } } - let (class_name_expr, tailwind_styles) = get_class_name_expression( - ast_builder, - &class_name_prop, - styles, - style_order, - &spread_props, - filename, - ); + let (class_name_expr, tailwind_styles) = + if let Some((condition, alt_styles, alt_style_order)) = conditional_branch { + // Conditional styleOrder: generate className for both branches + let (con_expr, con_tailwind) = get_class_name_expression( + ast_builder, + &class_name_prop, + styles, + style_order, + &spread_props, + filename, + ); + let alt_class_name_prop = class_name_prop + .as_ref() + .map(|c| c.clone_in(ast_builder.allocator)); + let (alt_expr, alt_tailwind) = get_class_name_expression( + ast_builder, + &alt_class_name_prop, + alt_styles, + alt_style_order, + &spread_props, + filename, + ); + + let combined_expr = + combine_conditional_class_name(ast_builder, condition, con_expr, alt_expr); + + let mut all_tailwind = con_tailwind; + all_tailwind.extend(alt_tailwind); + (combined_expr, all_tailwind) + } else { + get_class_name_expression( + ast_builder, + &class_name_prop, + styles, + style_order, + &spread_props, + filename, + ) + }; + if let Some(ex) = class_name_expr { props.push(ast_builder.object_property_kind_object_property( SPAN, @@ -99,6 +164,9 @@ pub fn modify_prop_object<'a>( } /// modify JSX props /// Returns extracted Tailwind styles from static className strings +/// `conditional_branch`: If Some, contains (condition, alternate_styles, alternate_style_order) +/// for generating a conditional className expression: `condition ? consequent_class : alternate_class` +#[allow(clippy::too_many_arguments)] pub fn modify_props<'a>( ast_builder: &AstBuilder<'a>, props: &mut oxc_allocator::Vec>, @@ -107,6 +175,7 @@ pub fn modify_props<'a>( style_vars: Option>, props_prop: Option>, filename: Option<&str>, + conditional_branch: Option<(Expression<'a>, &mut [ExtractStyleProp<'a>], Option)>, ) -> Vec { let mut class_name_prop = None; let mut style_prop = None; @@ -146,14 +215,45 @@ pub fn modify_props<'a>( } } } - let (class_name_expr, tailwind_styles) = get_class_name_expression( - ast_builder, - &class_name_prop, - styles, - style_order, - &spread_props, - filename, - ); + let (class_name_expr, tailwind_styles) = + if let Some((condition, alt_styles, alt_style_order)) = conditional_branch { + // Conditional styleOrder: generate className for both branches + let (con_expr, con_tailwind) = get_class_name_expression( + ast_builder, + &class_name_prop, + styles, + style_order, + &spread_props, + filename, + ); + let alt_class_name_prop = class_name_prop + .as_ref() + .map(|c| c.clone_in(ast_builder.allocator)); + let (alt_expr, alt_tailwind) = get_class_name_expression( + ast_builder, + &alt_class_name_prop, + alt_styles, + alt_style_order, + &spread_props, + filename, + ); + + let combined_expr = + combine_conditional_class_name(ast_builder, condition, con_expr, alt_expr); + + let mut all_tailwind = con_tailwind; + all_tailwind.extend(alt_tailwind); + (combined_expr, all_tailwind) + } else { + get_class_name_expression( + ast_builder, + &class_name_prop, + styles, + style_order, + &spread_props, + filename, + ) + }; if let Some(ex) = class_name_expr { props.push(ast_builder.jsx_attribute_item_attribute( SPAN, diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-2.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-2.snap new file mode 100644 index 00000000..02072ea8 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-2.snap @@ -0,0 +1,53 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-3.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-3.snap new file mode 100644 index 00000000..5d61f9f1 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-3.snap @@ -0,0 +1,29 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-4.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-4.snap new file mode 100644 index 00000000..75ea1e78 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-4.snap @@ -0,0 +1,57 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-5.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-5.snap new file mode 100644 index 00000000..a69c0ee1 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional-5.snap @@ -0,0 +1,43 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box, css} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "color", + value: "white", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional.snap new file mode 100644 index 00000000..cdc7984d --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional.snap @@ -0,0 +1,57 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_call_expression-2.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_call_expression-2.snap new file mode 100644 index 00000000..e216b140 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_call_expression-2.snap @@ -0,0 +1,57 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, { styleOrder: isActive ? 5 : 10, bg: isActive ? \"red\" : \"blue\" });\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", { className: isActive ? isActive ? \"a\" : \"b\" : isActive ? \"c\" : \"d\" });\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_call_expression-3.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_call_expression-3.snap new file mode 100644 index 00000000..99ac5d0c --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_call_expression-3.snap @@ -0,0 +1,33 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, { styleOrder: 5, bg: \"red\", p: \"4\" });\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", { className: \"a b\" });\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_call_expression.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_call_expression.snap new file mode 100644 index 00000000..7f4954e6 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_call_expression.snap @@ -0,0 +1,57 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, { styleOrder: isActive ? 5 : 10, bg: \"red\", p: \"4\" });\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", { className: isActive ? \"a b\" : \"c d\" });\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-2.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-2.snap new file mode 100644 index 00000000..07b2ca2e --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-2.snap @@ -0,0 +1,8 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Text} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: {}, + code: ";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-3.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-3.snap new file mode 100644 index 00000000..a0b77368 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-3.snap @@ -0,0 +1,57 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-4.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-4.snap new file mode 100644 index 00000000..3ce08c13 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-4.snap @@ -0,0 +1,8 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: {}, + code: "
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-5.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-5.snap new file mode 100644 index 00000000..0e678b60 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-5.snap @@ -0,0 +1,8 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, { styleOrder: isActive ? 5 : 10 });\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: {}, + code: "import { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", {});\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-6.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-6.snap new file mode 100644 index 00000000..8aedaa56 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-6.snap @@ -0,0 +1,19 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, { styleOrder: order, bg: \"red\" });\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", { className: \"a\" });\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-7.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-7.snap new file mode 100644 index 00000000..2b102046 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage-7.snap @@ -0,0 +1,19 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, { bg: \"red\" });\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", { className: \"a\" });\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage.snap new file mode 100644 index 00000000..af876c10 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_conditional_coverage.snap @@ -0,0 +1,105 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "bottom", + value: "0", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "bottom", + value: "0", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "left", + value: "0", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "left", + value: "0", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "right", + value: "0", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "right", + value: "0", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "top", + value: "0", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "top", + value: "0", + level: 0, + selector: None, + style_order: Some( + 10, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-2.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-2.snap new file mode 100644 index 00000000..30854da6 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-2.snap @@ -0,0 +1,33 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-3.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-3.snap new file mode 100644 index 00000000..25c51457 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-3.snap @@ -0,0 +1,33 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, { styleOrder: 5, bg: \"red\", p: 4 });\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", { className: \"a b\" });\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-4.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-4.snap new file mode 100644 index 00000000..611dc067 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-4.snap @@ -0,0 +1,57 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: Some( + 1, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: Some( + 2, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 1, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 2, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-5.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-5.snap new file mode 100644 index 00000000..11e771db --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-5.snap @@ -0,0 +1,29 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, { bg: \"red\", p: 4 });\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", { className: \"a b\" });\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-6.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-6.snap new file mode 100644 index 00000000..c5998f40 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional-6.snap @@ -0,0 +1,29 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#, ExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional.snap new file mode 100644 index 00000000..54764712 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_coverage_additional.snap @@ -0,0 +1,8 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, someVariable);\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: {}, + code: "import { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", someVariable);\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_logical_and-2.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_logical_and-2.snap new file mode 100644 index 00000000..a8c6d15d --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_logical_and-2.snap @@ -0,0 +1,53 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_logical_and-3.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_logical_and-3.snap new file mode 100644 index 00000000..892c6e7c --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_logical_and-3.snap @@ -0,0 +1,53 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/react\";\nfunction c() {\n return e(o, { styleOrder: a === 1 && 5, bg: \"red\", p: \"4\" });\n}\nexport { c as Lib };\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn e(\"div\", { className: a === 1 ? \"a b\" : \"c d\" });\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order_logical_and.snap b/libs/extractor/src/snapshots/extractor__tests__style_order_logical_and.snap new file mode 100644 index 00000000..a50ab4ff --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order_logical_and.snap @@ -0,0 +1,53 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: None, + style_order: Some( + 5, + ), + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/utils.rs b/libs/extractor/src/utils.rs index 67e4d610..2140cb0f 100644 --- a/libs/extractor/src/utils.rs +++ b/libs/extractor/src/utils.rs @@ -7,7 +7,7 @@ use oxc_ast::{ use oxc_codegen::{Codegen, CodegenOptions}; use oxc_parser::Parser; use oxc_span::{SPAN, SourceType}; -use oxc_syntax::operator::UnaryOperator; +use oxc_syntax::operator::{LogicalOperator, UnaryOperator}; /// Convert a value to a pixel value pub(super) fn convert_value(value: &str) -> String { @@ -56,6 +56,101 @@ pub(super) fn is_same_expression<'a>(a: &Expression<'a>, b: &Expression<'a>) -> } } +/// Represents a parsed styleOrder value that may be conditional +#[derive(Debug)] +pub(super) enum ParsedStyleOrder<'a> { + /// No styleOrder specified + None, + /// Static numeric value: styleOrder={5} + Static(u8), + /// Conditional: styleOrder={condition ? consequent : alternate} + Conditional { + condition: Expression<'a>, + consequent: Option, + alternate: Option, + }, +} + +impl ParsedStyleOrder<'_> { + /// Convert to Option for backward compatibility (returns None for Conditional) + pub fn as_static(&self) -> Option { + match self { + ParsedStyleOrder::Static(v) => Some(*v), + _ => None, + } + } +} + +/// Parse styleOrder from a JSX attribute value, supporting conditionals +pub(super) fn jsx_expression_to_style_order<'a>( + expr: &JSXAttributeValue<'a>, + allocator: &'a Allocator, +) -> ParsedStyleOrder<'a> { + // First try static resolution + if let Some(n) = jsx_expression_to_number(expr) { + return ParsedStyleOrder::Static(n as u8); + } + // Check for conditional or logical expression + if let JSXAttributeValue::ExpressionContainer(ec) = expr { + if let Some(Expression::ConditionalExpression(cond)) = ec.expression.as_expression() { + let consequent = get_number_by_literal_expression(&cond.consequent).map(|n| n as u8); + let alternate = get_number_by_literal_expression(&cond.alternate).map(|n| n as u8); + return ParsedStyleOrder::Conditional { + condition: cond.test.clone_in(allocator), + consequent, + alternate, + }; + } + // Handle logical &&: styleOrder={a === 1 && 5} + // truthy → right side (number), falsy → None (no styleOrder) + if let Some(Expression::LogicalExpression(logical)) = ec.expression.as_expression() + && logical.operator == LogicalOperator::And + { + let consequent = get_number_by_literal_expression(&logical.right).map(|n| n as u8); + return ParsedStyleOrder::Conditional { + condition: logical.left.clone_in(allocator), + consequent, + alternate: None, + }; + } + } + ParsedStyleOrder::None +} + +/// Parse styleOrder from an Expression (for call expression / object path), supporting conditionals +pub(super) fn expression_to_style_order<'a>( + expr: &Expression<'a>, + allocator: &'a Allocator, +) -> ParsedStyleOrder<'a> { + // First try static resolution + if let Some(n) = get_number_by_literal_expression(expr) { + return ParsedStyleOrder::Static(n as u8); + } + // Check for conditional expression + if let Expression::ConditionalExpression(cond) = expr { + let consequent = get_number_by_literal_expression(&cond.consequent).map(|n| n as u8); + let alternate = get_number_by_literal_expression(&cond.alternate).map(|n| n as u8); + return ParsedStyleOrder::Conditional { + condition: cond.test.clone_in(allocator), + consequent, + alternate, + }; + } + // Handle logical &&: styleOrder: a === 1 && 5 + // truthy → right side (number), falsy → None (no styleOrder) + if let Expression::LogicalExpression(logical) = expr + && logical.operator == LogicalOperator::And + { + let consequent = get_number_by_literal_expression(&logical.right).map(|n| n as u8); + return ParsedStyleOrder::Conditional { + condition: logical.left.clone_in(allocator), + consequent, + alternate: None, + }; + } + ParsedStyleOrder::None +} + pub(super) fn jsx_expression_to_number(expr: &JSXAttributeValue) -> Option { match expr { JSXAttributeValue::StringLiteral(sl) => get_number_by_literal_expression( diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index 4af7eaf1..65ef338f 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -25,8 +25,8 @@ use oxc_ast::ast::JSXAttributeItem::Attribute; use oxc_ast::ast::JSXAttributeName::Identifier; use oxc_ast::ast::{ Argument, BindingPattern, CallExpression, Expression, ImportDeclaration, ImportOrExportKind, - JSXAttributeItem, JSXAttributeValue, JSXChild, JSXElement, Program, Statement, - VariableDeclarator, WithClause, + JSXAttributeItem, JSXAttributeValue, JSXChild, JSXElement, ObjectPropertyKind, Program, + Statement, VariableDeclarator, WithClause, }; use oxc_ast_visit::VisitMut; use oxc_ast_visit::walk_mut::{ @@ -35,7 +35,10 @@ use oxc_ast_visit::walk_mut::{ }; use strum::IntoEnumIterator; -use crate::utils::{get_string_by_property_key, jsx_expression_to_number}; +use crate::utils::{ + ParsedStyleOrder, expression_to_style_order, get_string_by_property_key, + jsx_expression_to_style_order, +}; use oxc_ast::AstBuilder; use oxc_span::SPAN; use std::collections::{HashMap, HashSet}; @@ -368,6 +371,25 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { if let Some(kind) = element_kind && it.arguments.len() > 1 { + // Pre-scan: detect conditional styleOrder before extract_style_from_expression + // consumes the property (which only handles static values) + let parsed_style_order = + if let Expression::ObjectExpression(obj) = it.arguments[1].to_expression() { + obj.properties.iter().find_map(|prop| { + if let ObjectPropertyKind::ObjectProperty(p) = prop + && let Some(name) = get_string_by_property_key(&p.key) + && name == "styleOrder" + { + Some(expression_to_style_order(&p.value, self.ast.allocator)) + } else { + None + } + }) + } else { + None + } + .unwrap_or(ParsedStyleOrder::None); + let mut tag = self.ast .expression_string_literal(SPAN, self.ast.atom(kind.to_tag()), None); @@ -397,27 +419,89 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { .rev() .map(ExtractStyleProp::Static), ); - props_styles.iter().rev().for_each(|style| { - self.styles.extend(style.extract().into_iter().map(|mut s| { - style_order.into_iter().for_each(|order| { - s.set_style_order(order); - }); - s - })) - }); - if let Expression::ObjectExpression(obj) = it.arguments[1].to_expression_mut() { - let tailwind_styles = modify_prop_object( - &self.ast, - &mut obj.properties, - &mut props_styles, - style_order, - style_vars, - props, - self.split_filename.as_deref(), - ); - // Add extracted Tailwind styles to the visitor's style set - self.styles.extend(tailwind_styles); + // Use pre-scanned ParsedStyleOrder, falling back to extract_style_from_expression's + // static result for backward compat. + // Note: pre-scan and extract_style_from_expression both use get_number_by_literal_expression + // on the same value, so style_order is always None when parsed_style_order is None. + let parsed_style_order = match parsed_style_order { + ParsedStyleOrder::None => { + style_order.map_or(ParsedStyleOrder::None, ParsedStyleOrder::Static) + } + other => other, + }; + + if let ParsedStyleOrder::Conditional { + condition, + consequent, + alternate, + } = &parsed_style_order + { + // Clone styles for alternate branch before consequent processing mutates them + let mut alt_props_styles: Vec> = props_styles + .iter() + .map(|s| s.clone_in(self.ast.allocator)) + .collect(); + + if let Expression::ObjectExpression(obj) = it.arguments[1].to_expression_mut() { + let tailwind_styles = modify_prop_object( + &self.ast, + &mut obj.properties, + &mut props_styles, + *consequent, + style_vars, + props, + self.split_filename.as_deref(), + Some(( + condition.clone_in(self.ast.allocator), + &mut alt_props_styles, + *alternate, + )), + ); + self.styles.extend(tailwind_styles); + } + + // Collect styles from both branches for CSS output + props_styles.iter().rev().for_each(|style| { + self.styles.extend(style.extract().into_iter().map(|mut s| { + if let Some(order) = consequent { + s.set_style_order(*order); + } + s + })) + }); + alt_props_styles.iter().rev().for_each(|style| { + self.styles.extend(style.extract().into_iter().map(|mut s| { + if let Some(order) = alternate { + s.set_style_order(*order); + } + s + })) + }); + } else { + let style_order = parsed_style_order.as_static(); + props_styles.iter().rev().for_each(|style| { + self.styles.extend(style.extract().into_iter().map(|mut s| { + style_order.into_iter().for_each(|order| { + s.set_style_order(order); + }); + s + })) + }); + + if let Expression::ObjectExpression(obj) = it.arguments[1].to_expression_mut() { + let tailwind_styles = modify_prop_object( + &self.ast, + &mut obj.properties, + &mut props_styles, + style_order, + style_vars, + props, + self.split_filename.as_deref(), + None, + ); + self.styles.extend(tailwind_styles); + } } it.arguments[0] = Argument::from(tag); @@ -555,7 +639,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { // extract ExtractStyleProp and remain style and class name, just extract let mut duplicate_set = HashSet::new(); - let mut style_order = None; + let mut parsed_style_order = ParsedStyleOrder::None; let mut style_vars = None; let mut props = None; for i in (0..attrs.len()).rev() { @@ -569,9 +653,10 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { if !duplicate_set.contains(&name) { duplicate_set.insert(name.clone()); if property_name == "styleOrder" { - style_order = - jsx_expression_to_number(attr.value.as_ref().unwrap()) - .map(|n| n as u8); + parsed_style_order = jsx_expression_to_style_order( + attr.value.as_ref().unwrap(), + self.ast.allocator, + ); } else if property_name == "props" { if let Some(value) = attr.value.as_ref() && let JSXAttributeValue::ExpressionContainer(expr) = value @@ -618,23 +703,71 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { .rev() .for_each(|ex| props_styles.push(ExtractStyleProp::Static(ex))); - let tailwind_styles = modify_props( - &self.ast, - attrs, - &mut props_styles, - style_order, - style_vars, - props, - self.split_filename.as_deref(), - ); - // Add extracted Tailwind styles to the visitor's style set - self.styles.extend(tailwind_styles); - - props_styles - .iter() - .rev() - .for_each(|style| self.styles.extend(style.extract())); - // modify!! + if let ParsedStyleOrder::Conditional { + condition, + consequent, + alternate, + } = &parsed_style_order + { + // Clone styles for alternate branch before consequent processing mutates them + let mut alt_props_styles: Vec> = props_styles + .iter() + .map(|s| s.clone_in(self.ast.allocator)) + .collect(); + + // Process consequent branch + let tailwind_styles_con = modify_props( + &self.ast, + attrs, + &mut props_styles, + *consequent, + style_vars, + props, + self.split_filename.as_deref(), + Some(( + condition.clone_in(self.ast.allocator), + &mut alt_props_styles, + *alternate, + )), + ); + self.styles.extend(tailwind_styles_con); + + // Collect styles from both branches for CSS output + props_styles.iter().rev().for_each(|style| { + self.styles.extend(style.extract().into_iter().map(|mut s| { + if let Some(order) = consequent { + s.set_style_order(*order); + } + s + })) + }); + alt_props_styles.iter().rev().for_each(|style| { + self.styles.extend(style.extract().into_iter().map(|mut s| { + if let Some(order) = alternate { + s.set_style_order(*order); + } + s + })) + }); + } else { + let style_order = parsed_style_order.as_static(); + let tailwind_styles = modify_props( + &self.ast, + attrs, + &mut props_styles, + style_order, + style_vars, + props, + self.split_filename.as_deref(), + None, + ); + self.styles.extend(tailwind_styles); + + props_styles + .iter() + .rev() + .for_each(|style| self.styles.extend(style.extract())); + } if let Some(tag) = if let Expression::StringLiteral(str) = tag_name { Some(str.value.as_str()) diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml index f6027e3d..35e11548 100644 --- a/libs/sheet/Cargo.toml +++ b/libs/sheet/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" css = { path = "../css" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -regex = "1.12.2" +regex = "1.12.3" once_cell = "1.21.3" extractor = { path = "../extractor" } diff --git a/package.json b/package.json index 2c3275c7..65467013 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "lint": "cargo fmt --all -- --check && cargo clippy --all-targets --all-features -- -D warnings && eslint", - "lint:fix": "eslint --fix", + "lint:fix": "eslint --fix && cargo fmt", "pretest": "bun run --filter @devup-ui/vite-plugin build", "test": "cargo tarpaulin --out xml --out stdout --out html --all-targets --engine llvm && bun test", "build": "bun run --filter @devup-ui/wasm --filter @devup-ui/plugin-utils build && bun run --filter @devup-ui/react --filter @devup-ui/webpack-plugin build && bun run --filter @devup-ui/eslint-plugin --filter @devup-ui/vite-plugin --filter @devup-ui/next-plugin --filter @devup-ui/rsbuild-plugin --filter @devup-ui/bun-plugin --filter @devup-ui/components --filter @devup-ui/reset-css build",