Skip to main content

stylex_transform/shared/utils/core/
define_vars_utils.rs

1use std::rc::Rc;
2
3use indexmap::IndexMap;
4use stylex_macros::stylex_panic;
5use swc_core::ecma::ast::{Expr, Lit};
6
7use crate::shared::enums::data_structures::flat_compiled_styles_value::FlatCompiledStylesValue;
8use crate::shared::structures::types::{
9  ClassPathsInNamespace, FlatCompiledStyles, InjectableStylesMap,
10};
11use crate::shared::utils::ast::convertors::{convert_key_value_to_str, convert_lit_to_string};
12use crate::shared::utils::common::{
13  create_hash, get_key_values_from_object, round_to_decimal_places,
14};
15use stylex_constants::constants::common::SPLIT_TOKEN;
16use stylex_constants::constants::messages::{EXPECTED_CSS_VAR, VALUES_MUST_BE_OBJECT};
17use stylex_enums::value_with_default::ValueWithDefault;
18use stylex_types::structures::injectable_style::InjectableStyle;
19
20pub(crate) fn construct_css_variables_string(
21  variables: &FlatCompiledStyles,
22  theme_name_hash: &String,
23  typed_variables: &mut FlatCompiledStyles,
24) -> InjectableStylesMap {
25  let mut rules_by_at_rule = IndexMap::new();
26
27  for (key, value) in variables.iter() {
28    collect_vars_by_at_rules(key, value, &mut rules_by_at_rule, &[], typed_variables);
29  }
30
31  let mut result: InjectableStylesMap = IndexMap::new();
32
33  for (at_rule, value) in rules_by_at_rule.iter() {
34    let suffix = if at_rule == "default" {
35      String::default()
36    } else {
37      format!("-{}", create_hash(at_rule))
38    };
39
40    let selector = format!(":root, .{theme_name_hash}");
41
42    let mut ltr = format!("{selector}{{{}}}", value.join(""));
43
44    if at_rule != "default" {
45      ltr = wrap_with_at_rules(ltr.as_str(), at_rule);
46    }
47
48    result.insert(
49      format!("{}{}", theme_name_hash, suffix),
50      InjectableStyle::regular(
51        ltr,
52        // Round to avoid floating-point precision issues (0.1 + 0.2 = 0.30000000000000004)
53        Some(round_to_decimal_places(
54          priority_for_at_rule(at_rule) / 10.0,
55          1,
56        )),
57      ),
58    );
59  }
60
61  result
62}
63
64pub(crate) fn collect_vars_by_at_rules(
65  key: &String,
66  value: &FlatCompiledStylesValue,
67  collection: &mut ClassPathsInNamespace,
68  at_rules: &[String],
69  typed_variables: &mut FlatCompiledStyles,
70) {
71  let Some((hash_name, value, css_type)) = value.as_tuple() else {
72    stylex_panic!("{}", VALUES_MUST_BE_OBJECT)
73  };
74
75  if let Some(css_type) = css_type {
76    let values = match css_type.value.as_map() {
77      Some(v) => v,
78      None => stylex_panic!("Value must be a map"),
79    };
80
81    let initial_value = get_nitial_value_of_css_type(values);
82
83    typed_variables.insert(
84      hash_name.clone(),
85      Rc::new(FlatCompiledStylesValue::CSSType(
86        hash_name.clone(),
87        css_type.syntax,
88        initial_value.clone(),
89      )),
90    );
91  }
92
93  match value {
94    Expr::Array(_) => stylex_panic!(
95      "Array values are not supported in defineVars(). Use a string, number, or nested object."
96    ),
97    Expr::Lit(lit) => {
98      if let Lit::Null(_) = lit {
99        return;
100      }
101
102      let val = match convert_lit_to_string(lit) {
103        Some(v) => v,
104        None => stylex_panic!("{}", EXPECTED_CSS_VAR),
105      };
106
107      let key = if at_rules.is_empty() {
108        "default".to_string()
109      } else {
110        let mut keys = at_rules.to_vec();
111        keys.sort();
112        keys.join(SPLIT_TOKEN)
113      };
114
115      collection
116        .entry(key)
117        .or_default()
118        .push(format!("--{}:{};", hash_name, val));
119    },
120    Expr::Object(obj) => {
121      let key_values = get_key_values_from_object(obj);
122
123      if !key_values.iter().any(|key_value| {
124        let key = convert_key_value_to_str(key_value);
125
126        key == "default"
127      }) {
128        stylex_panic!(r#"Default value is not defined for "{}" variable."#, key);
129      }
130
131      for key_value in key_values.iter() {
132        let at_rule = convert_key_value_to_str(key_value);
133
134        let extended_at_rules = if at_rule == "default" {
135          at_rules.to_vec()
136        } else {
137          let mut new_at_rule = at_rules.to_vec();
138          new_at_rule.push(at_rule.clone());
139          new_at_rule
140        };
141
142        let value = key_value.value.clone();
143
144        collect_vars_by_at_rules(
145          &at_rule,
146          &FlatCompiledStylesValue::Tuple(hash_name.clone(), value, None),
147          collection,
148          &extended_at_rules,
149          typed_variables,
150        );
151      }
152    },
153    _ => {},
154  }
155}
156
157fn get_nitial_value_of_css_type(values: &IndexMap<String, ValueWithDefault>) -> String {
158  values
159    .get("default")
160    .map(|value| match value {
161      ValueWithDefault::Number(num) => num.to_string(),
162      ValueWithDefault::String(strng) => strng.clone(),
163      ValueWithDefault::Map(map) => get_nitial_value_of_css_type(map),
164    })
165    .unwrap_or_else(|| stylex_panic!("CSS type requires a default value but none was provided."))
166}
167
168pub(crate) fn wrap_with_at_rules(ltr: &str, at_rule: &str) -> String {
169  at_rule
170    .split(SPLIT_TOKEN)
171    .fold(ltr.to_string(), |acc, at_rule| {
172      format!("{}{{{}}}", at_rule, acc)
173    })
174}
175
176pub(crate) fn priority_for_at_rule(at_rule: &str) -> f64 {
177  if at_rule == "default" {
178    1.0
179  } else {
180    1.0 + at_rule.split(SPLIT_TOKEN).count() as f64
181  }
182}