Skip to content

Commit 78708a8

Browse files
committed
Support stylex
1 parent 5776b5c commit 78708a8

34 files changed

Lines changed: 2147 additions & 11 deletions

File tree

libs/extractor/src/extractor/extract_style_from_stylex.rs

Lines changed: 290 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
use crate::ExtractStyleProp;
2+
use crate::extract_style::extract_dynamic_style::ExtractDynamicStyle;
23
use crate::extract_style::extract_static_style::ExtractStaticStyle;
34
use crate::extract_style::extract_style_value::ExtractStyleValue;
45
use crate::stylex::{
5-
SelectorPart, decompose_value_conditions, format_number, is_unitless_property,
6-
normalize_stylex_property,
6+
SelectorPart, decompose_value_conditions, format_number, is_first_that_works_call,
7+
is_types_call, is_unitless_property, normalize_stylex_property,
78
};
89
use crate::utils::{get_number_by_literal_expression, get_string_by_literal_expression};
910
use css::optimize_value::optimize_value;
11+
use css::sheet_to_variable_name;
1012
use oxc_ast::AstBuilder;
11-
use oxc_ast::ast::{Expression, ObjectPropertyKind};
13+
use oxc_ast::ast::{BindingPattern, Expression, ObjectPropertyKind, Statement};
14+
use rustc_hash::FxHashMap;
1215

1316
use crate::utils::get_string_by_property_key;
1417

@@ -18,10 +21,16 @@ use crate::utils::get_string_by_property_key;
1821
///
1922
/// Returns a Vec of `(namespace_name, style_props)` pairs. Each namespace
2023
/// corresponds to a top-level key in the `stylex.create({...})` argument.
24+
#[allow(clippy::type_complexity)]
2125
pub fn extract_stylex_namespace_styles<'a>(
2226
_ast_builder: &AstBuilder<'a>,
2327
expression: &mut Expression<'a>,
24-
) -> Vec<(String, Vec<ExtractStyleProp<'a>>)> {
28+
keyframe_names: &FxHashMap<String, String>,
29+
) -> Vec<(
30+
String,
31+
Vec<ExtractStyleProp<'a>>,
32+
Option<Vec<(usize, String)>>,
33+
)> {
2534
let Expression::ObjectExpression(obj) = expression else {
2635
return vec![];
2736
};
@@ -30,27 +39,63 @@ pub fn extract_stylex_namespace_styles<'a>(
3039

3140
for prop in obj.properties.iter() {
3241
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
42+
// Phase 4c: Spread not supported at namespace level
43+
if matches!(prop, ObjectPropertyKind::SpreadProperty(_)) {
44+
eprintln!(
45+
"[stylex] ERROR: Object spread is not allowed at the namespace level of stylex.create()."
46+
);
47+
}
3348
continue;
3449
};
3550

3651
let Some(ns_name) = get_string_by_property_key(&prop.key) else {
52+
// Phase 4c: Computed namespace keys not supported
53+
if prop.computed {
54+
eprintln!(
55+
"[stylex] ERROR: Computed namespace keys are not allowed in stylex.create()."
56+
);
57+
}
3758
continue;
3859
};
3960

61+
// Phase 4b: Arrow function (dynamic namespace)
62+
if let Expression::ArrowFunctionExpression(arrow) = &prop.value {
63+
if let Some((styles, css_vars)) =
64+
extract_stylex_dynamic_namespace(arrow, keyframe_names)
65+
{
66+
result.push((ns_name, styles, Some(css_vars)));
67+
} else {
68+
result.push((ns_name, vec![], None));
69+
}
70+
continue;
71+
}
72+
4073
let Expression::ObjectExpression(ns_obj) = &prop.value else {
4174
// Non-object namespace value (e.g., null): push empty styles
42-
result.push((ns_name, vec![]));
75+
result.push((ns_name, vec![], None));
4376
continue;
4477
};
4578

4679
let mut styles = vec![];
4780

4881
for style_prop in ns_obj.properties.iter() {
4982
let ObjectPropertyKind::ObjectProperty(style_prop) = style_prop else {
83+
// Phase 4c: Spread not supported in style properties
84+
if matches!(style_prop, ObjectPropertyKind::SpreadProperty(_)) {
85+
eprintln!(
86+
"[stylex] ERROR: Object spread is not allowed in stylex.create() namespaces. Define all properties explicitly."
87+
);
88+
}
5089
continue;
5190
};
5291

5392
let Some(prop_name) = get_string_by_property_key(&style_prop.key) else {
93+
// Phase 4c: Computed property keys not supported
94+
if style_prop.computed {
95+
eprintln!(
96+
"[stylex] ERROR: Computed property keys are not allowed in stylex.create(). Use static string keys instead."
97+
);
98+
}
5499
continue;
55100
};
56101

@@ -92,6 +137,51 @@ pub fn extract_stylex_namespace_styles<'a>(
92137

93138
let css_property = normalize_stylex_property(&prop_name);
94139

140+
// Phase 4c: Warn about CSS shorthand properties
141+
const SHORTHAND_PROPERTIES: &[&str] = &[
142+
"margin",
143+
"padding",
144+
"background",
145+
"border",
146+
"font",
147+
"outline",
148+
"overflow",
149+
"flex",
150+
"grid",
151+
"gap",
152+
"border-radius",
153+
"border-color",
154+
"border-style",
155+
"border-width",
156+
"margin-inline",
157+
"margin-block",
158+
"padding-inline",
159+
"padding-block",
160+
];
161+
if SHORTHAND_PROPERTIES.contains(&css_property.as_str()) {
162+
eprintln!(
163+
"[stylex] WARNING: Shorthand property '{}' may cause unexpected specificity issues. Consider using longhand properties (e.g., 'marginTop', 'paddingLeft').",
164+
css_property
165+
);
166+
}
167+
168+
// Phase 4a: Resolve keyframe variable references (e.g., animationName: fadeIn)
169+
if let Expression::Identifier(ident) = &style_prop.value
170+
&& let Some(anim_name) = keyframe_names.get(ident.name.as_str())
171+
{
172+
styles.push(ExtractStyleProp::Static(ExtractStyleValue::Static(
173+
ExtractStaticStyle {
174+
property: css_property,
175+
value: optimize_value(anim_name),
176+
level: 0,
177+
selector: None,
178+
style_order: None,
179+
layer: None,
180+
},
181+
)));
182+
continue;
183+
}
184+
95185
// Phase 1: static string/number values
96186
let css_value = if let Some(s) = get_string_by_literal_expression(&style_prop.value) {
97187
s
@@ -119,8 +209,79 @@ pub fn extract_stylex_namespace_styles<'a>(
119209
}
120210
}
121211
continue;
212+
} else if let Expression::CallExpression(call) = &style_prop.value
213+
&& is_first_that_works_call(&call.callee)
214+
{
215+
// firstThatWorks('a', 'b', 'c'): last arg is least preferred, first is most preferred.
216+
// CSS fallback: output in reverse order (least preferred first, most preferred last).
217+
for arg in call.arguments.iter().rev() {
218+
let arg_expr = arg.to_expression();
219+
if let Some(s) = get_string_by_literal_expression(arg_expr) {
220+
styles.push(ExtractStyleProp::Static(ExtractStyleValue::Static(
221+
ExtractStaticStyle {
222+
property: css_property.clone(),
223+
value: optimize_value(&s),
224+
level: 0,
225+
selector: None,
226+
style_order: None,
227+
layer: None,
228+
},
229+
)));
230+
} else if let Some(n) = get_number_by_literal_expression(arg_expr) {
231+
let formatted = if is_unitless_property(&css_property) || n == 0.0 {
232+
format_number(n)
233+
} else {
234+
format!("{}px", format_number(n))
235+
};
236+
styles.push(ExtractStyleProp::Static(ExtractStyleValue::Static(
237+
ExtractStaticStyle {
238+
property: css_property.clone(),
239+
value: optimize_value(&formatted),
240+
level: 0,
241+
selector: None,
242+
style_order: None,
243+
layer: None,
244+
},
245+
)));
246+
}
247+
}
248+
continue;
249+
} else if let Expression::CallExpression(call) = &style_prop.value
250+
&& is_types_call(&call.callee)
251+
&& !call.arguments.is_empty()
252+
{
253+
// stylex.types.length('100px') → extract inner value '100px'
254+
let inner = call.arguments[0].to_expression();
255+
let css_value = if let Some(s) = get_string_by_literal_expression(inner) {
256+
s
257+
} else if let Some(n) = get_number_by_literal_expression(inner) {
258+
if is_unitless_property(&css_property) || n == 0.0 {
259+
format_number(n)
260+
} else {
261+
format!("{}px", format_number(n))
262+
}
263+
} else {
264+
continue; // Can't resolve inner value
265+
};
266+
styles.push(ExtractStyleProp::Static(ExtractStyleValue::Static(
267+
ExtractStaticStyle {
268+
property: css_property,
269+
value: optimize_value(&css_value),
270+
level: 0,
271+
selector: None,
272+
style_order: None,
273+
layer: None,
274+
},
275+
)));
276+
continue;
122277
} else {
123-
// Skip NullLiteral, dynamic values, etc.
278+
// Phase 4c: Non-static values in create() are not supported
279+
if !matches!(&style_prop.value, Expression::NullLiteral(_)) {
280+
eprintln!(
281+
"[stylex] ERROR: Non-static value for property '{}' in stylex.create(). Only string literals, numbers, null, objects (conditions), firstThatWorks(), types.*(), and arrow functions are allowed.",
282+
prop_name
283+
);
284+
}
124285
continue;
125286
};
126287

@@ -138,8 +299,130 @@ pub fn extract_stylex_namespace_styles<'a>(
138299
)));
139300
}
140301

141-
result.push((ns_name, styles));
302+
result.push((ns_name, styles, None));
142303
}
143304

144305
result
145306
}
307+
308+
/// Extract styles from a dynamic StyleX namespace (arrow function).
309+
/// Returns (styles_for_css, css_vars) where css_vars maps param_index to CSS variable name.
310+
#[allow(clippy::type_complexity)]
311+
fn extract_stylex_dynamic_namespace<'a>(
312+
arrow: &oxc_ast::ast::ArrowFunctionExpression<'a>,
313+
keyframe_names: &FxHashMap<String, String>,
314+
) -> Option<(Vec<ExtractStyleProp<'a>>, Vec<(usize, String)>)> {
315+
// 1. Extract parameter names
316+
let param_names: Vec<String> = arrow
317+
.params
318+
.items
319+
.iter()
320+
.filter_map(|param| {
321+
if let BindingPattern::BindingIdentifier(ident) = &param.pattern {
322+
Some(ident.name.to_string())
323+
} else {
324+
None
325+
}
326+
})
327+
.collect();
328+
329+
if param_names.is_empty() {
330+
return None;
331+
}
332+
333+
// 2. Get body ObjectExpression from expression body: (x) => ({ ... })
334+
if !arrow.expression {
335+
return None;
336+
}
337+
let stmt = arrow.body.statements.first()?;
338+
let Statement::ExpressionStatement(expr_stmt) = stmt else {
339+
return None;
340+
};
341+
// Handle both direct ObjectExpression and ParenthesizedExpression wrapping
342+
let body_obj = match &expr_stmt.expression {
343+
Expression::ObjectExpression(obj) => obj,
344+
Expression::ParenthesizedExpression(paren) => {
345+
if let Expression::ObjectExpression(obj) = &paren.expression {
346+
obj
347+
} else {
348+
return None;
349+
}
350+
}
351+
_ => return None,
352+
};
353+
354+
// 3. Process each property
355+
let mut styles = vec![];
356+
let mut css_vars = vec![];
357+
358+
for prop in body_obj.properties.iter() {
359+
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
360+
continue;
361+
};
362+
363+
let Some(prop_name) = get_string_by_property_key(&prop.key) else {
364+
continue;
365+
};
366+
let css_property = normalize_stylex_property(&prop_name);
367+
368+
// Check if value references a parameter (dynamic)
369+
let is_dynamic = if prop.shorthand {
370+
// Shorthand: { height } is equivalent to { height: height }
371+
param_names.iter().position(|p| p == &prop_name)
372+
} else if let Expression::Identifier(ident) = &prop.value {
373+
param_names.iter().position(|p| p == ident.name.as_str())
374+
} else {
375+
None
376+
};
377+
378+
if let Some(param_idx) = is_dynamic {
379+
// Dynamic property: generate CSS variable
380+
let var_name = sheet_to_variable_name(&css_property, 0, None);
381+
css_vars.push((param_idx, var_name));
382+
let param_name = &param_names[param_idx];
383+
styles.push(ExtractStyleProp::Static(ExtractStyleValue::Dynamic(
384+
ExtractDynamicStyle::new(&css_property, 0, param_name, None),
385+
)));
386+
} else {
387+
// Static property: resolve keyframe references or literal values
388+
if let Expression::Identifier(ident) = &prop.value
389+
&& let Some(anim_name) = keyframe_names.get(ident.name.as_str())
390+
{
391+
styles.push(ExtractStyleProp::Static(ExtractStyleValue::Static(
392+
ExtractStaticStyle {
393+
property: css_property,
394+
value: optimize_value(anim_name),
395+
level: 0,
396+
selector: None,
397+
style_order: None,
398+
layer: None,
399+
},
400+
)));
401+
continue;
402+
}
403+
let css_value = if let Some(s) = get_string_by_literal_expression(&prop.value) {
404+
s
405+
} else if let Some(n) = get_number_by_literal_expression(&prop.value) {
406+
if is_unitless_property(&css_property) || n == 0.0 {
407+
format_number(n)
408+
} else {
409+
format!("{}px", format_number(n))
410+
}
411+
} else {
412+
continue;
413+
};
414+
styles.push(ExtractStyleProp::Static(ExtractStyleValue::Static(
415+
ExtractStaticStyle {
416+
property: css_property,
417+
value: optimize_value(&css_value),
418+
level: 0,
419+
selector: None,
420+
style_order: None,
421+
layer: None,
422+
},
423+
)));
424+
}
425+
}
426+
427+
Some((styles, css_vars))
428+
}

0 commit comments

Comments
 (0)