11use crate :: ExtractStyleProp ;
2+ use crate :: extract_style:: extract_dynamic_style:: ExtractDynamicStyle ;
23use crate :: extract_style:: extract_static_style:: ExtractStaticStyle ;
34use crate :: extract_style:: extract_style_value:: ExtractStyleValue ;
45use 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} ;
89use crate :: utils:: { get_number_by_literal_expression, get_string_by_literal_expression} ;
910use css:: optimize_value:: optimize_value;
11+ use css:: sheet_to_variable_name;
1012use 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
1316use 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) ]
2125pub 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