@@ -41,6 +41,19 @@ pub struct StyleEntry {
4141 pub bases : Vec < String > ,
4242}
4343
44+ /// Entry for createGlobalTheme() - CSS variables scoped to a selector
45+ #[ derive( Debug , Clone , Default ) ]
46+ pub struct GlobalThemeEntry {
47+ /// CSS selector (e.g., ":root")
48+ pub selector : String ,
49+ /// CSS variables: Vec<(var_name, value)> e.g. [("--color-brand-0-0", "blue")]
50+ pub css_vars : Vec < ( String , String ) > ,
51+ /// Serialized JS object with var() references
52+ pub vars_object_json : String ,
53+ /// Whether this is exported
54+ pub exported : bool ,
55+ }
56+
4457/// Collected style definitions from vanilla-extract API calls
4558#[ derive( Debug , Default ) ]
4659pub struct CollectedStyles {
@@ -60,6 +73,8 @@ pub struct CollectedStyles {
6073 pub containers : HashMap < String , ( String , bool ) > ,
6174 /// layer() calls: variable_name -> (layer name string, exported)
6275 pub layers : HashMap < String , ( String , bool ) > ,
76+ /// createGlobalTheme() calls: variable_name -> GlobalThemeEntry
77+ pub global_themes : HashMap < String , GlobalThemeEntry > ,
6378 /// Non-style constant exports: variable_name -> value (as code string)
6479 pub constant_exports : HashMap < String , String > ,
6580}
@@ -75,6 +90,7 @@ struct StyleCollectorInner {
7590 styles : CollectedStyles ,
7691 style_counter : usize ,
7792 font_counter : usize ,
93+ global_theme_counter : usize ,
7894}
7995
8096type StyleCollector = Rc < RefCell < StyleCollectorInner > > ;
@@ -93,6 +109,13 @@ fn next_font_id(collector: &StyleCollector) -> String {
93109 id
94110}
95111
112+ fn next_global_theme_id ( collector : & StyleCollector ) -> String {
113+ let mut inner = collector. borrow_mut ( ) ;
114+ let id = format ! ( "__global_theme_{}__" , inner. global_theme_counter) ;
115+ inner. global_theme_counter += 1 ;
116+ id
117+ }
118+
96119/// Parse font-face JSON and return list of (key, value) pairs
97120/// Input: {"src":"local(...)","fontWeight":400}
98121/// Output: vec![("src", "\"local(...)\""), ("fontWeight", "400")]
@@ -121,6 +144,80 @@ fn parse_font_face_json(json: &str) -> Vec<(String, String)> {
121144 . collect ( )
122145}
123146
147+ /// Recursively transform theme object to CSS var() references
148+ /// Returns a new JS object with the same structure but leaf values replaced with var(--path)
149+ fn transform_theme_to_vars (
150+ value : & JsValue ,
151+ ctx : & mut Context ,
152+ placeholder_id : & str ,
153+ css_vars : & mut Vec < ( String , String ) > ,
154+ var_counter : & mut usize ,
155+ path : & [ String ] ,
156+ ) -> JsValue {
157+ if let Some ( obj) = value. as_object ( ) {
158+ // Check if it's an array (shouldn't happen in theme objects, but handle it)
159+ if obj. is_array ( ) {
160+ return value. clone ( ) ;
161+ }
162+
163+ // It's an object - recursively transform each property
164+ let new_obj = boa_engine:: object:: ObjectInitializer :: new ( ctx) . build ( ) ;
165+
166+ // Get own property keys
167+ if let Ok ( keys) = obj. own_property_keys ( ctx) {
168+ for key in keys {
169+ // Convert PropertyKey to string
170+ let key_string = match & key {
171+ boa_engine:: property:: PropertyKey :: String ( s) => s. to_std_string_escaped ( ) ,
172+ boa_engine:: property:: PropertyKey :: Symbol ( _) => continue ,
173+ boa_engine:: property:: PropertyKey :: Index ( i) => i. get ( ) . to_string ( ) ,
174+ } ;
175+
176+ if let Ok ( prop_value) = obj. get ( js_string ! ( key_string. as_str( ) ) , ctx) {
177+ let mut new_path = path. to_vec ( ) ;
178+ new_path. push ( key_string. clone ( ) ) ;
179+
180+ let transformed = transform_theme_to_vars (
181+ & prop_value,
182+ ctx,
183+ placeholder_id,
184+ css_vars,
185+ var_counter,
186+ & new_path,
187+ ) ;
188+
189+ let _ = new_obj. set ( js_string ! ( key_string. as_str( ) ) , transformed, false , ctx) ;
190+ }
191+ }
192+ }
193+
194+ JsValue :: from ( new_obj)
195+ } else {
196+ // Leaf value - create CSS variable
197+ let var_name = format ! (
198+ "--{}-{}-{}" ,
199+ path. join( "-" ) ,
200+ placeholder_id. trim_matches( '_' ) . replace( "__" , "-" ) ,
201+ var_counter
202+ ) ;
203+ * var_counter += 1 ;
204+
205+ // Get the actual value as string
206+ let value_str = if let Some ( s) = value. as_string ( ) {
207+ s. to_std_string_escaped ( )
208+ } else if let Ok ( s) = value. to_string ( ctx) {
209+ s. to_std_string_escaped ( )
210+ } else {
211+ String :: new ( )
212+ } ;
213+
214+ css_vars. push ( ( var_name. clone ( ) , value_str) ) ;
215+
216+ // Return var(--name)
217+ JsValue :: from ( js_string ! ( format!( "var({})" , var_name) ) )
218+ }
219+ }
220+
124221/// Convert JsValue to JSON string using JSON.stringify
125222fn js_value_to_json ( value : & JsValue , context : & mut Context ) -> String {
126223 // Use JSON.stringify to convert the value
@@ -271,6 +368,7 @@ fn is_style_api_call(expr: &oxc_ast::ast::Expression) -> bool {
271368 | "createVar"
272369 | "createContainer"
273370 | "layer"
371+ | "createGlobalTheme"
274372 ) ;
275373 }
276374 false
@@ -293,8 +391,10 @@ fn remap_style_names(
293391 let mut new_containers = HashMap :: new ( ) ;
294392 let mut new_layers = HashMap :: new ( ) ;
295393 let mut new_font_faces = HashMap :: new ( ) ;
394+ let mut new_global_themes = HashMap :: new ( ) ;
296395 let mut style_idx = 0 ;
297396 let mut font_idx = 0 ;
397+ let mut global_theme_idx = 0 ;
298398
299399 // First pass: collect old entries preserving all fields
300400 let old_styles: HashMap < String , StyleEntry > = collected. styles . drain ( ) . collect ( ) ;
@@ -318,6 +418,9 @@ fn remap_style_names(
318418 . drain ( )
319419 . map ( |( k, v) | ( k, ( v. 0 , v. 1 ) ) )
320420 . collect ( ) ;
421+ // global_themes: placeholder_id -> GlobalThemeEntry (without exported flag for remapping)
422+ let old_global_themes: HashMap < String , GlobalThemeEntry > =
423+ collected. global_themes . drain ( ) . collect ( ) ;
321424
322425 for ( name, info) in vars {
323426 match info {
@@ -332,6 +435,22 @@ fn remap_style_names(
332435 continue ;
333436 }
334437
438+ // Check if this is a createGlobalTheme (uses __global_theme_N__ placeholder)
439+ let global_theme_placeholder = format ! ( "__global_theme_{}__" , global_theme_idx) ;
440+ if let Some ( entry) = old_global_themes. get ( & global_theme_placeholder) {
441+ new_global_themes. insert (
442+ name. clone ( ) ,
443+ GlobalThemeEntry {
444+ selector : entry. selector . clone ( ) ,
445+ css_vars : entry. css_vars . clone ( ) ,
446+ vars_object_json : entry. vars_object_json . clone ( ) ,
447+ exported : * exported,
448+ } ,
449+ ) ;
450+ global_theme_idx += 1 ;
451+ continue ;
452+ }
453+
335454 let placeholder = format ! ( "__style_{}__" , style_idx) ;
336455 placeholder_to_name. insert ( placeholder. clone ( ) , name. clone ( ) ) ;
337456
@@ -433,6 +552,7 @@ fn remap_style_names(
433552 collected. containers = new_containers;
434553 collected. layers = new_layers;
435554 collected. font_faces = new_font_faces;
555+ collected. global_themes = new_global_themes;
436556}
437557
438558/// Convert TypeScript to JavaScript using Oxc Transformer and replace imports
@@ -749,10 +869,48 @@ fn register_vanilla_extract_apis(
749869 } ;
750870
751871 // createGlobalTheme() function
872+ let collector_global_theme = collector. clone ( ) ;
752873 let create_global_theme_fn = unsafe {
753- NativeFunction :: from_closure ( move |_this, args, _ctx| {
874+ NativeFunction :: from_closure ( move |_this, args, ctx| {
875+ let placeholder_id = next_global_theme_id ( & collector_global_theme) ;
876+ let selector = args
877+ . get_or_undefined ( 0 )
878+ . to_string ( ctx)
879+ . map ( |s| s. to_std_string_escaped ( ) )
880+ . unwrap_or_else ( |_| ":root" . to_string ( ) ) ;
754881 let theme_obj = args. get_or_undefined ( 1 ) ;
755- Ok ( theme_obj. clone ( ) )
882+
883+ // Collect CSS variables and build new object with var() references
884+ let mut css_vars = Vec :: new ( ) ;
885+ let mut var_counter = 0usize ;
886+ let result_obj = transform_theme_to_vars (
887+ theme_obj,
888+ ctx,
889+ & placeholder_id,
890+ & mut css_vars,
891+ & mut var_counter,
892+ & [ ] ,
893+ ) ;
894+
895+ // Serialize the result object to JSON for code generation
896+ let vars_object_json = js_value_to_json ( & result_obj, ctx) ;
897+
898+ // Store the collected CSS variables and vars object
899+ collector_global_theme
900+ . borrow_mut ( )
901+ . styles
902+ . global_themes
903+ . insert (
904+ placeholder_id,
905+ GlobalThemeEntry {
906+ selector,
907+ css_vars,
908+ vars_object_json,
909+ exported : false ,
910+ } ,
911+ ) ;
912+
913+ Ok ( result_obj)
756914 } )
757915 } ;
758916
@@ -794,7 +952,10 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
794952 if !collected. styles . is_empty ( ) || !collected. style_variants . is_empty ( ) {
795953 imports. push ( "css" ) ;
796954 }
797- if !collected. global_styles . is_empty ( ) || !collected. font_faces . is_empty ( ) {
955+ if !collected. global_styles . is_empty ( )
956+ || !collected. font_faces . is_empty ( )
957+ || !collected. global_themes . is_empty ( )
958+ {
798959 imports. push ( "globalCss" ) ;
799960 }
800961 if !collected. keyframes . is_empty ( ) {
@@ -893,6 +1054,25 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
8931054 code_parts. push ( code) ;
8941055 }
8951056
1057+ // Generate createGlobalTheme CSS variables via globalCss (sorted for deterministic output)
1058+ let mut global_themes_sorted: Vec < _ > = collected. global_themes . iter ( ) . collect ( ) ;
1059+ global_themes_sorted. sort_by_key ( |( name, _) | * name) ;
1060+ for ( _name, entry) in & global_themes_sorted {
1061+ if !entry. css_vars . is_empty ( ) {
1062+ // Build CSS variables object for the selector
1063+ let vars_str = entry
1064+ . css_vars
1065+ . iter ( )
1066+ . map ( |( var_name, value) | format ! ( "\" {}\" : \" {}\" " , var_name, value) )
1067+ . collect :: < Vec < _ > > ( )
1068+ . join ( ", " ) ;
1069+ code_parts. push ( format ! (
1070+ "globalCss({{ \" {}\" : {{ {} }} }})" ,
1071+ entry. selector, vars_str
1072+ ) ) ;
1073+ }
1074+ }
1075+
8961076 // Generate keyframes declarations (sorted for deterministic output)
8971077 let mut keyframes: Vec < _ > = collected. keyframes . iter ( ) . collect ( ) ;
8981078 keyframes. sort_by_key ( |( name, _) | * name) ;
@@ -991,6 +1171,15 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
9911171 code_parts. push ( format ! ( "{}const {} = \" {}\" " , prefix, name, value) ) ;
9921172 }
9931173
1174+ // Generate createGlobalTheme vars object declarations (sorted for deterministic output)
1175+ for ( name, entry) in & global_themes_sorted {
1176+ let prefix = if entry. exported { "export " } else { "" } ;
1177+ code_parts. push ( format ! (
1178+ "{}const {} = {}" ,
1179+ prefix, name, entry. vars_object_json
1180+ ) ) ;
1181+ }
1182+
9941183 // Generate constant exports (sorted for deterministic output)
9951184 let mut constants: Vec < _ > = collected. constant_exports . iter ( ) . collect ( ) ;
9961185 constants. sort_by_key ( |( name, _) | * name) ;
@@ -1012,6 +1201,7 @@ impl Clone for CollectedStyles {
10121201 style_variants : self . style_variants . clone ( ) ,
10131202 containers : self . containers . clone ( ) ,
10141203 layers : self . layers . clone ( ) ,
1204+ global_themes : self . global_themes . clone ( ) ,
10151205 constant_exports : self . constant_exports . clone ( ) ,
10161206 }
10171207 }
0 commit comments