Skip to content

Commit 737f501

Browse files
committed
Support stylex
1 parent 78708a8 commit 737f501

11 files changed

Lines changed: 464 additions & 18 deletions

libs/extractor/src/extractor/extract_style_from_stylex.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ use crate::extract_style::extract_dynamic_style::ExtractDynamicStyle;
33
use crate::extract_style::extract_static_style::ExtractStaticStyle;
44
use crate::extract_style::extract_style_value::ExtractStyleValue;
55
use crate::stylex::{
6-
SelectorPart, decompose_value_conditions, format_number, is_first_that_works_call,
7-
is_types_call, is_unitless_property, normalize_stylex_property,
6+
SelectorPart, StylexIncludeRef, decompose_value_conditions, format_number,
7+
is_first_that_works_call, is_include_call_static, is_types_call, is_unitless_property,
8+
normalize_stylex_property,
89
};
910
use crate::utils::{get_number_by_literal_expression, get_string_by_literal_expression};
1011
use css::optimize_value::optimize_value;
@@ -19,7 +20,7 @@ use crate::utils::get_string_by_property_key;
1920
///
2021
/// Handles static string/number values (Phase 1) and value-level conditions (Phase 2).
2122
///
22-
/// Returns a Vec of `(namespace_name, style_props)` pairs. Each namespace
23+
/// Returns a Vec of `(namespace_name, style_props, css_vars, include_refs)` tuples. Each namespace
2324
/// corresponds to a top-level key in the `stylex.create({...})` argument.
2425
#[allow(clippy::type_complexity)]
2526
pub fn extract_stylex_namespace_styles<'a>(
@@ -30,6 +31,7 @@ pub fn extract_stylex_namespace_styles<'a>(
3031
String,
3132
Vec<ExtractStyleProp<'a>>,
3233
Option<Vec<(usize, String)>>,
34+
Vec<StylexIncludeRef>,
3335
)> {
3436
let Expression::ObjectExpression(obj) = expression else {
3537
return vec![];
@@ -63,25 +65,41 @@ pub fn extract_stylex_namespace_styles<'a>(
6365
if let Some((styles, css_vars)) =
6466
extract_stylex_dynamic_namespace(arrow, keyframe_names)
6567
{
66-
result.push((ns_name, styles, Some(css_vars)));
68+
result.push((ns_name, styles, Some(css_vars), vec![]));
6769
} else {
68-
result.push((ns_name, vec![], None));
70+
result.push((ns_name, vec![], None, vec![]));
6971
}
7072
continue;
7173
}
7274

7375
let Expression::ObjectExpression(ns_obj) = &prop.value else {
7476
// Non-object namespace value (e.g., null): push empty styles
75-
result.push((ns_name, vec![], None));
77+
result.push((ns_name, vec![], None, vec![]));
7678
continue;
7779
};
7880

7981
let mut styles = vec![];
82+
let mut include_refs = vec![];
8083

8184
for style_prop in ns_obj.properties.iter() {
8285
let ObjectPropertyKind::ObjectProperty(style_prop) = style_prop else {
83-
// Phase 4c: Spread not supported in style properties
84-
if matches!(style_prop, ObjectPropertyKind::SpreadProperty(_)) {
86+
// Check for stylex.include() spread
87+
if let ObjectPropertyKind::SpreadProperty(spread) = style_prop
88+
&& let Expression::CallExpression(call) = &spread.argument
89+
&& is_include_call_static(&call.callee)
90+
&& !call.arguments.is_empty()
91+
{
92+
// Parse include(base.member)
93+
if let Expression::StaticMemberExpression(member) =
94+
call.arguments[0].to_expression()
95+
&& let Expression::Identifier(ident) = &member.object
96+
{
97+
include_refs.push(StylexIncludeRef {
98+
var_name: ident.name.to_string(),
99+
member_name: member.property.name.to_string(),
100+
});
101+
}
102+
} else if matches!(style_prop, ObjectPropertyKind::SpreadProperty(_)) {
85103
eprintln!(
86104
"[stylex] ERROR: Object spread is not allowed in stylex.create() namespaces. Define all properties explicitly."
87105
);
@@ -299,7 +317,7 @@ pub fn extract_stylex_namespace_styles<'a>(
299317
)));
300318
}
301319

302-
result.push((ns_name, styles, None));
320+
result.push((ns_name, styles, None, include_refs));
303321
}
304322

305323
result

libs/extractor/src/lib.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15288,4 +15288,115 @@ const styles = stylex.create({
1528815288
.unwrap()
1528915289
));
1529015290
}
15291+
15292+
#[test]
15293+
#[serial]
15294+
fn test_stylex_types_in_condition() {
15295+
reset_class_map();
15296+
reset_file_map();
15297+
assert_debug_snapshot!(ToBTreeSet::from(
15298+
extract(
15299+
"test.tsx",
15300+
r#"import stylex from '@stylexjs/stylex';
15301+
const styles = stylex.create({
15302+
base: {
15303+
width: {
15304+
default: '100%',
15305+
'@media (min-width: 768px)': stylex.types.length('50%'),
15306+
},
15307+
},
15308+
});"#,
15309+
ExtractOption {
15310+
package: "@devup-ui/react".to_string(),
15311+
css_dir: "@devup-ui/react".to_string(),
15312+
single_css: true,
15313+
import_main_css: false,
15314+
import_aliases: HashMap::new()
15315+
},
15316+
)
15317+
.unwrap()
15318+
));
15319+
}
15320+
15321+
#[test]
15322+
#[serial]
15323+
fn test_stylex_include_basic() {
15324+
reset_class_map();
15325+
reset_file_map();
15326+
assert_debug_snapshot!(ToBTreeSet::from(
15327+
extract(
15328+
"test.tsx",
15329+
r#"import stylex from '@stylexjs/stylex';
15330+
const base = stylex.create({
15331+
root: { color: 'red', fontSize: '16px' },
15332+
});
15333+
const composed = stylex.create({
15334+
fancy: { ...stylex.include(base.root), backgroundColor: 'blue' },
15335+
});"#,
15336+
ExtractOption {
15337+
package: "@devup-ui/react".to_string(),
15338+
css_dir: "@devup-ui/react".to_string(),
15339+
single_css: true,
15340+
import_main_css: false,
15341+
import_aliases: HashMap::new()
15342+
},
15343+
)
15344+
.unwrap()
15345+
));
15346+
}
15347+
15348+
#[test]
15349+
#[serial]
15350+
fn test_stylex_include_named_import() {
15351+
reset_class_map();
15352+
reset_file_map();
15353+
assert_debug_snapshot!(ToBTreeSet::from(
15354+
extract(
15355+
"test.tsx",
15356+
r#"import { create, include } from '@stylexjs/stylex';
15357+
const base = create({
15358+
root: { color: 'red' },
15359+
});
15360+
const composed = create({
15361+
fancy: { ...include(base.root), padding: '8px' },
15362+
});"#,
15363+
ExtractOption {
15364+
package: "@devup-ui/react".to_string(),
15365+
css_dir: "@devup-ui/react".to_string(),
15366+
single_css: true,
15367+
import_main_css: false,
15368+
import_aliases: HashMap::new()
15369+
},
15370+
)
15371+
.unwrap()
15372+
));
15373+
}
15374+
15375+
#[test]
15376+
#[serial]
15377+
fn test_stylex_include_with_props() {
15378+
reset_class_map();
15379+
reset_file_map();
15380+
assert_debug_snapshot!(ToBTreeSet::from(
15381+
extract(
15382+
"test.tsx",
15383+
r#"import stylex from '@stylexjs/stylex';
15384+
const base = stylex.create({
15385+
root: { color: 'red' },
15386+
});
15387+
const composed = stylex.create({
15388+
fancy: { ...stylex.include(base.root), backgroundColor: 'blue' },
15389+
});
15390+
const el = <div {...stylex.props(composed.fancy)} />;"#,
15391+
ExtractOption {
15392+
package: "@devup-ui/react".to_string(),
15393+
css_dir: "@devup-ui/react".to_string(),
15394+
single_css: true,
15395+
import_main_css: false,
15396+
import_aliases: HashMap::new()
15397+
},
15398+
)
15399+
.unwrap()
15400+
));
15401+
}
1529115402
}

libs/extractor/src/snapshots/extractor__tests__stylex_first_that_works_in_condition.snap

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@ ToBTreeSet {
1414
layer: None,
1515
},
1616
),
17+
Static(
18+
ExtractStaticStyle {
19+
property: "display",
20+
value: "flex",
21+
level: 0,
22+
selector: Some(
23+
Selector(
24+
"&:hover",
25+
),
26+
),
27+
style_order: None,
28+
layer: None,
29+
},
30+
),
31+
Static(
32+
ExtractStaticStyle {
33+
property: "display",
34+
value: "grid",
35+
level: 0,
36+
selector: Some(
37+
Selector(
38+
"&:hover",
39+
),
40+
),
41+
style_order: None,
42+
layer: None,
43+
},
44+
),
1745
},
18-
code: "import \"@devup-ui/react/devup-ui.css\";\nimport stylex from \"@stylexjs/stylex\";\nconst styles = { \"base\": \"a\" };\n",
46+
code: "import \"@devup-ui/react/devup-ui.css\";\nimport stylex from \"@stylexjs/stylex\";\nconst styles = { \"base\": \"a b c\" };\n",
1947
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
source: libs/extractor/src/lib.rs
3+
expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import stylex from '@stylexjs/stylex';\nconst base = stylex.create({\n root: { color: 'red', fontSize: '16px' },\n});\nconst composed = stylex.create({\n fancy: { ...stylex.include(base.root), backgroundColor: 'blue' },\n});\"#,\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())"
4+
---
5+
ToBTreeSet {
6+
styles: {
7+
Static(
8+
ExtractStaticStyle {
9+
property: "background-color",
10+
value: "blue",
11+
level: 0,
12+
selector: None,
13+
style_order: None,
14+
layer: None,
15+
},
16+
),
17+
Static(
18+
ExtractStaticStyle {
19+
property: "color",
20+
value: "red",
21+
level: 0,
22+
selector: None,
23+
style_order: None,
24+
layer: None,
25+
},
26+
),
27+
Static(
28+
ExtractStaticStyle {
29+
property: "font-size",
30+
value: "16px",
31+
level: 0,
32+
selector: None,
33+
style_order: None,
34+
layer: None,
35+
},
36+
),
37+
},
38+
code: "import \"@devup-ui/react/devup-ui.css\";\nimport stylex from \"@stylexjs/stylex\";\nconst base = { \"root\": \"a b\" };\nconst composed = { \"fancy\": \"a b c\" };\n",
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
source: libs/extractor/src/lib.rs
3+
expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { create, include } from '@stylexjs/stylex';\nconst base = create({\n root: { color: 'red' },\n});\nconst composed = create({\n fancy: { ...include(base.root), padding: '8px' },\n});\"#,\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())"
4+
---
5+
ToBTreeSet {
6+
styles: {
7+
Static(
8+
ExtractStaticStyle {
9+
property: "color",
10+
value: "red",
11+
level: 0,
12+
selector: None,
13+
style_order: None,
14+
layer: None,
15+
},
16+
),
17+
Static(
18+
ExtractStaticStyle {
19+
property: "padding",
20+
value: "8px",
21+
level: 0,
22+
selector: None,
23+
style_order: None,
24+
layer: None,
25+
},
26+
),
27+
},
28+
code: "import \"@devup-ui/react/devup-ui.css\";\nimport { create, include } from \"@stylexjs/stylex\";\nconst base = { \"root\": \"a\" };\nconst composed = { \"fancy\": \"a b\" };\n",
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
source: libs/extractor/src/lib.rs
3+
expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import stylex from '@stylexjs/stylex';\nconst base = stylex.create({\n root: { color: 'red' },\n});\nconst composed = stylex.create({\n fancy: { ...stylex.include(base.root), backgroundColor: 'blue' },\n});\nconst el = <div {...stylex.props(composed.fancy)} />;\"#,\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())"
4+
---
5+
ToBTreeSet {
6+
styles: {
7+
Static(
8+
ExtractStaticStyle {
9+
property: "background-color",
10+
value: "blue",
11+
level: 0,
12+
selector: None,
13+
style_order: None,
14+
layer: None,
15+
},
16+
),
17+
Static(
18+
ExtractStaticStyle {
19+
property: "color",
20+
value: "red",
21+
level: 0,
22+
selector: None,
23+
style_order: None,
24+
layer: None,
25+
},
26+
),
27+
},
28+
code: "import \"@devup-ui/react/devup-ui.css\";\nimport stylex from \"@stylexjs/stylex\";\nconst base = { \"root\": \"a\" };\nconst composed = { \"fancy\": \"a b\" };\nconst el = <div {...{ className: \"a b\" }} />;\n",
29+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
source: libs/extractor/src/lib.rs
3+
expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import stylex from '@stylexjs/stylex';\nconst styles = stylex.create({\n base: {\n width: {\n default: '100%',\n '@media (min-width: 768px)': stylex.types.length('50%'),\n },\n },\n});\"#,\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())"
4+
---
5+
ToBTreeSet {
6+
styles: {
7+
Static(
8+
ExtractStaticStyle {
9+
property: "width",
10+
value: "100%",
11+
level: 0,
12+
selector: None,
13+
style_order: None,
14+
layer: None,
15+
},
16+
),
17+
Static(
18+
ExtractStaticStyle {
19+
property: "width",
20+
value: "50%",
21+
level: 0,
22+
selector: Some(
23+
At {
24+
kind: Media,
25+
query: "(min-width: 768px)",
26+
selector: None,
27+
},
28+
),
29+
style_order: None,
30+
layer: None,
31+
},
32+
),
33+
},
34+
code: "import \"@devup-ui/react/devup-ui.css\";\nimport stylex from \"@stylexjs/stylex\";\nconst styles = { \"base\": \"a b\" };\n",
35+
}

0 commit comments

Comments
 (0)