Skip to main content

stylex_compiler_rs/enums/
mod.rs

1use fancy_regex::Regex;
2use log::warn;
3use napi::JsValue;
4use napi::{
5  Env, Error, NapiRaw, Unknown,
6  bindgen_prelude::{FromNapiValue, ToNapiValue},
7  sys::{napi_env, napi_value},
8};
9use napi_derive::napi;
10use stylex_regex::regex::NPM_NAME_REGEX;
11use stylex_structures::named_import_source::NamedImportSource;
12
13#[napi(object)]
14#[derive(Debug, Clone)]
15pub struct StyleXModuleResolution {
16  pub r#type: String,
17  pub root_dir: Option<String>,
18  pub theme_file_extension: Option<String>,
19}
20
21#[napi(string_enum)]
22#[derive(Debug)]
23pub enum SourceMaps {
24  True,
25  False,
26  Inline,
27}
28
29#[napi(object)]
30#[derive(Debug, serde::Serialize, serde::Deserialize)]
31pub struct ImportSourceInput {
32  #[serde(rename = "as")]
33  pub as_: String,
34  pub from: String,
35}
36
37#[derive(Debug, Clone)]
38pub enum ImportSourceUnion {
39  Regular(String),
40  Named(NamedImportSource),
41}
42
43#[derive(Debug, Clone)]
44pub enum RuntimeInjectionUnion {
45  Boolean(bool),
46  Regular(String),
47}
48
49impl FromNapiValue for RuntimeInjectionUnion {
50  unsafe fn from_napi_value(env: napi_env, value: napi::sys::napi_value) -> Result<Self, Error> {
51    // Try to parse as boolean first
52    if let Ok(bool_value) = unsafe { bool::from_napi_value(env, value) } {
53      return Ok(RuntimeInjectionUnion::Boolean(bool_value));
54    }
55
56    // Fall back to string
57    let js_unknown = unsafe { Unknown::from_napi_value(env, value) }?;
58    let js_str = unsafe { js_unknown.cast::<napi::JsString>() }?;
59    let string_value = js_str.into_utf8()?.as_str()?.to_owned();
60
61    Ok(RuntimeInjectionUnion::Regular(string_value))
62  }
63}
64
65impl ToNapiValue for RuntimeInjectionUnion {
66  unsafe fn to_napi_value(env: napi_env, value: Self) -> Result<napi_value, Error> {
67    match value {
68      RuntimeInjectionUnion::Boolean(b) => unsafe { bool::to_napi_value(env, b) },
69      RuntimeInjectionUnion::Regular(s) => {
70        let env = Env::from_raw(env);
71        let js_str = env.create_string(&s)?;
72        Ok(js_str.raw())
73      },
74    }
75  }
76}
77
78static MAX_IMPORT_PATH_LENGTH: usize = 214;
79
80fn validate_import_path(path: &str) -> Result<(), String> {
81  if path.len() > MAX_IMPORT_PATH_LENGTH {
82    return Err(format!(
83      "Import path is too long (max {} characters)",
84      MAX_IMPORT_PATH_LENGTH
85    ));
86  }
87
88  if !NPM_NAME_REGEX.is_match(path).unwrap_or_else(|err| {
89    warn!(
90      "Error matching NPM_NAME_REGEX for '{}': {}. Skipping pattern match.",
91      path, err
92    );
93
94    false
95  }) {
96    return Err("Import path does not match required pattern".to_string());
97  }
98  Ok(())
99}
100
101impl FromNapiValue for ImportSourceUnion {
102  unsafe fn from_napi_value(env: napi_env, value: napi::sys::napi_value) -> Result<Self, Error> {
103    let js_unknown = unsafe { Unknown::from_napi_value(env, value) }?;
104    // SAFETY: This cast will fail if the value is not actually a JsObject,
105    // which is handled by the Err branch below.
106    let js_obj = unsafe { js_unknown.cast::<napi::JsObject>() };
107
108    match js_obj {
109      Ok(obj) => match unsafe { ImportSourceInput::from_napi_value(env, obj.raw()) } {
110        Ok(value) => {
111          validate_import_path(&value.from).map_err(Error::from_reason)?;
112          Ok(ImportSourceUnion::Named(NamedImportSource {
113            r#as: value.as_,
114            from: value.from,
115          }))
116        },
117        Err(_) => {
118          let js_unknown = unsafe { Unknown::from_napi_value(env, value) }?;
119          let js_str = unsafe { js_unknown.cast::<napi::JsString>() }?;
120          let import_path = js_str.into_utf8()?.as_str()?.to_owned();
121
122          validate_import_path(&import_path).map_err(Error::from_reason)?;
123          Ok(ImportSourceUnion::Regular(import_path))
124        },
125      },
126      Err(_) => {
127        let js_unknown = unsafe { Unknown::from_napi_value(env, value) }?;
128        let js_str = unsafe { js_unknown.cast::<napi::JsString>() }?;
129        let import_path = js_str.into_utf8()?.as_str()?.to_owned();
130
131        validate_import_path(&import_path).map_err(Error::from_reason)?;
132        Ok(ImportSourceUnion::Regular(import_path))
133      },
134    }
135  }
136}
137
138impl ToNapiValue for ImportSourceUnion {
139  unsafe fn to_napi_value(env: napi_env, value: Self) -> Result<napi_value, Error> {
140    match value {
141      ImportSourceUnion::Regular(s) => {
142        let env = Env::from_raw(env);
143        let js_str = env.create_string(&s)?;
144        Ok(js_str.raw())
145      },
146      ImportSourceUnion::Named(named) => {
147        let env = Env::from_raw(env);
148        let mut js_obj = env.create_object()?;
149
150        let as_str = env.create_string(&named.r#as)?;
151        let from_str = env.create_string(&named.from)?;
152
153        js_obj.set_named_property("as", as_str)?;
154        js_obj.set_named_property("from", from_str)?;
155
156        Ok(unsafe { js_obj.raw() })
157      },
158    }
159  }
160}
161
162#[derive(Debug, Clone)]
163pub enum PathFilterUnion {
164  Glob(String),
165  Regex(String),
166}
167
168impl PathFilterUnion {
169  pub fn from_string(pattern: &str) -> Self {
170    if pattern.starts_with('/') && pattern.len() > 2 {
171      // Find the last unescaped slash to handle patterns like /path\/to\/file/
172      let mut last_slash_pos = None;
173      let chars: Vec<char> = pattern.chars().collect();
174
175      for i in (1..chars.len()).rev() {
176        if chars[i] == '/' {
177          // Check if this slash is escaped (preceded by odd number of backslashes)
178          let mut backslash_count = 0;
179          let mut j = i;
180          while j > 0 && chars[j - 1] == '\\' {
181            backslash_count += 1;
182            j -= 1;
183          }
184
185          // If even number of backslashes (including 0), the slash is not escaped
186          if backslash_count % 2 == 0 {
187            last_slash_pos = Some(i);
188            break;
189          }
190        }
191      }
192
193      if let Some(last_slash) = last_slash_pos {
194        // Extract the regex pattern (without the surrounding slashes)
195        let regex_pattern = &pattern[1..last_slash];
196        let flags = &pattern[last_slash + 1..];
197
198        // Validate regex flags (only valid JS regex flags: gimsuy)
199        if flags
200          .chars()
201          .all(|c| matches!(c, 'g' | 'i' | 'm' | 's' | 'u' | 'y'))
202        {
203          // Try to validate the regex pattern
204          if Regex::new(regex_pattern).is_ok() {
205            // Convert JS flags to inline modifiers for Rust regex
206            let mut inline_flags = String::new();
207            if flags.contains('i') {
208              inline_flags.push('i');
209            }
210            if flags.contains('m') {
211              inline_flags.push('m');
212            }
213            if flags.contains('s') {
214              inline_flags.push('s');
215            }
216
217            let final_pattern = if !inline_flags.is_empty() {
218              format!("(?{}){}", inline_flags, regex_pattern)
219            } else {
220              regex_pattern.to_string()
221            };
222
223            return PathFilterUnion::Regex(final_pattern);
224          }
225        }
226      }
227    }
228
229    // Default to glob pattern
230    PathFilterUnion::Glob(pattern.to_string())
231  }
232}
233
234#[napi(string_enum)]
235#[derive(Debug)]
236pub enum PropertyValidationMode {
237  #[napi(value = "throw")]
238  Throw,
239  #[napi(value = "warn")]
240  Warn,
241  #[napi(value = "silent")]
242  Silent,
243}
244
245/// Represents the `sxPropName` option: a string name for the sx prop, or `false` to disable.
246#[derive(Debug, Clone)]
247pub enum SxPropNameUnion {
248  /// Disables the `sx` prop feature
249  Disabled,
250  /// A string name for the sx prop (e.g. `"sx"` or `"css"`)
251  Name(String),
252}
253
254impl FromNapiValue for SxPropNameUnion {
255  unsafe fn from_napi_value(env: napi_env, value: napi::sys::napi_value) -> Result<Self, Error> {
256    // Try to parse as boolean first
257    if let Ok(bool_value) = unsafe { bool::from_napi_value(env, value) } {
258      // Only allow `false` to disable the feature - `true` is an error
259      if bool_value {
260        return Err(Error::from_reason(
261          "sxPropName does not accept `true` - use `false` to disable or provide a string prop name",
262        ));
263      }
264      return Ok(SxPropNameUnion::Disabled);
265    }
266
267    // Fall back to string
268    let js_unknown = unsafe { Unknown::from_napi_value(env, value) }?;
269    let js_str = unsafe { js_unknown.cast::<napi::JsString>() }?;
270    let string_value = js_str.into_utf8()?.as_str()?.to_owned();
271
272    Ok(SxPropNameUnion::Name(string_value))
273  }
274}
275
276impl ToNapiValue for SxPropNameUnion {
277  unsafe fn to_napi_value(env: napi_env, value: Self) -> Result<napi_value, Error> {
278    match value {
279      SxPropNameUnion::Disabled => unsafe { bool::to_napi_value(env, false) },
280      SxPropNameUnion::Name(s) => {
281        let env = Env::from_raw(env);
282        let js_str = env.create_string(&s)?;
283        Ok(js_str.raw())
284      },
285    }
286  }
287}