Skip to main content

stylex_compiler_rs/
lib.rs

1#![allow(deprecated)]
2
3mod enums;
4mod structs;
5mod utils;
6use log::info;
7use napi::ValueType;
8use napi::{Env, Result};
9use std::panic;
10use std::{env, sync::Arc};
11use structs::{StyleXMetadata, StyleXOptions, StyleXTransformResult};
12use stylex_logs::initializer::initialize as initialize_logger;
13use stylex_macros::stylex_error::{SuppressPanicStderr, format_panic_message};
14use swc_compiler_base::{PrintArgs, SourceMapsConfig, print};
15
16use stylex_enums::style_resolution::StyleResolution;
17use stylex_structures::{plugin_pass::PluginPass, stylex_options::StyleXOptionsParams};
18use stylex_transform::StyleXTransform;
19use swc_ecma_parser::{Parser, StringInput, Syntax, TsSyntax, lexer::Lexer};
20
21use swc_core::{
22  common::{FileName, GLOBALS, Globals, Mark, SourceMap},
23  ecma::{
24    ast::EsVersion,
25    transforms::{
26      base::{fixer::fixer, hygiene::hygiene, resolver},
27      typescript::strip as typescript_strip,
28    },
29    visit::fold_pass,
30  },
31  plugin::proxies::PluginCommentsProxy,
32};
33
34use napi_derive::napi;
35use utils::extract_stylex_metadata;
36
37use crate::enums::{
38  ImportSourceUnion, PathFilterUnion, PropertyValidationMode, RuntimeInjectionUnion, SourceMaps,
39  StyleXModuleResolution,
40};
41
42fn extract_patterns(
43  env: &Env,
44  patterns_opt: &mut Option<Vec<napi::UnknownRef>>,
45) -> Option<Vec<PathFilterUnion>> {
46  patterns_opt.take().map(|patterns| {
47    patterns
48      .into_iter()
49      .filter_map(|p| match p.get_value(env) {
50        Ok(unknown) => parse_js_pattern_from_unknown(env, unknown).ok(),
51        Err(e) => {
52          info!(
53            "Failed to get value from UnknownRef in extract_patterns: {:?}",
54            e
55          );
56          None
57        },
58      })
59      .collect()
60  })
61}
62
63#[napi]
64pub fn transform(
65  env: Env,
66  filename: String,
67  code: String,
68  mut options: StyleXOptions,
69) -> Result<StyleXTransformResult> {
70  initialize_logger();
71
72  info!("Transforming source file: {}", filename);
73
74  let mut include_opt = options.include.take();
75  let mut exclude_opt = options.exclude.take();
76  let include_patterns = extract_patterns(&env, &mut include_opt);
77  let exclude_patterns = extract_patterns(&env, &mut exclude_opt);
78
79  // Parse the env object separately since it needs the napi::Env for JS function references.
80  let parsed_env = options
81    .env
82    .take()
83    .map(|ref env_obj| utils::fn_parser::parse_env_object(&env, env_obj))
84    .transpose()?;
85
86  // Parse debugFilePath separately since it needs the napi::Env for JS function references.
87  let parsed_debug_file_path = options
88    .debug_file_path
89    .take()
90    .map(|unknown_ref| {
91      let value = unknown_ref.get_value(&env)?;
92      utils::fn_parser::parse_debug_file_path(&env, value)
93    })
94    .transpose()?;
95
96  if !utils::should_transform_file(&filename, &include_patterns, &exclude_patterns) {
97    return Ok(StyleXTransformResult {
98      code,
99      metadata: StyleXMetadata { stylex: vec![] },
100      map: None,
101    });
102  }
103
104  let _suppress = SuppressPanicStderr::new();
105  let result = panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
106    let cm: Arc<SourceMap> = Default::default();
107    let filename = FileName::Real(filename.into());
108
109    let fm = cm.new_source_file(filename.clone().into(), code);
110
111    let cwd = env::current_dir()?;
112
113    let plugin_pass = PluginPass {
114      cwd: Some(cwd),
115      filename: filename.clone(),
116    };
117
118    let source_map = match options.source_map.as_ref() {
119      Some(SourceMaps::True) => SourceMapsConfig::Bool(true),
120      Some(SourceMaps::False) => SourceMapsConfig::Bool(false),
121      Some(SourceMaps::Inline) => SourceMapsConfig::Str("inline".to_string()),
122      None => SourceMapsConfig::Bool(true),
123    };
124
125    let mut config: StyleXOptionsParams = options.try_into()?;
126
127    // Set the parsed env and debugFilePath on the config
128    config.env = parsed_env;
129    config.debug_file_path = parsed_debug_file_path;
130
131    let mut parser = Parser::new_from(Lexer::new(
132      Syntax::Typescript(TsSyntax {
133        tsx: true,
134        ..Default::default()
135      }),
136      EsVersion::latest(),
137      StringInput::from(&*fm),
138      None,
139    ));
140
141    let program = match parser.parse_program() {
142      Ok(program) => program,
143      Err(err) => {
144        let error_message = format!("Failed to parse file `{}`: {:?}", filename, err);
145        return Err(napi::Error::from_reason(error_message));
146      },
147    };
148
149    let globals = Globals::default();
150    GLOBALS.set(&globals, || {
151      // Set the NAPI env in thread-local storage so env functions can call back to JS
152      utils::fn_parser::with_napi_env(&env, || {
153        let unresolved_mark = Mark::new();
154        let top_level_mark = Mark::new();
155
156        let mut stylex: StyleXTransform<PluginCommentsProxy> =
157          StyleXTransform::new(PluginCommentsProxy, plugin_pass, &mut config);
158
159        let program = program
160          .apply(resolver(unresolved_mark, top_level_mark, true))
161          .apply(typescript_strip(unresolved_mark, top_level_mark))
162          .apply(&mut fold_pass(&mut stylex))
163          .apply(hygiene())
164          .apply(&mut fixer(None));
165
166        let stylex_metadata = extract_stylex_metadata(env, &stylex)?;
167
168        let transformed_code = print(
169          cm,
170          &program,
171          PrintArgs {
172            source_map,
173            ..Default::default()
174          },
175        );
176
177        let result = match transformed_code {
178          Ok(output) => output,
179          Err(e) => {
180            return Err(napi::Error::from_reason(format!(
181              "[StyleX] Failed to print transformed code: {}",
182              e
183            )));
184          },
185        };
186
187        let js_result = StyleXTransformResult {
188          code: result.code,
189          metadata: StyleXMetadata {
190            stylex: stylex_metadata,
191          },
192          map: result.map,
193        };
194
195        Ok(js_result)
196      })
197    })
198  }));
199
200  match result {
201    Ok(res) => res,
202    Err(error) => {
203      let error_msg = format_panic_message(&error);
204
205      Err(napi::Error::from_reason(error_msg))
206    },
207  }
208}
209
210#[napi]
211pub fn should_transform_file(
212  env: Env,
213  file_path: String,
214  include: Option<napi::JsObject>,
215  exclude: Option<napi::JsObject>,
216) -> Result<bool> {
217  let include_patterns = include.and_then(|arr| {
218    let mut parsed = Vec::new();
219    if let Ok(len) = arr.get_array_length() {
220      for i in 0..len {
221        if let Ok(elem) = arr.get_element::<napi::Unknown>(i)
222          && let Ok(pattern) = parse_js_pattern_from_unknown(&env, elem)
223        {
224          parsed.push(pattern);
225        }
226      }
227    }
228    if parsed.is_empty() {
229      None
230    } else {
231      Some(parsed)
232    }
233  });
234
235  let exclude_patterns = exclude.and_then(|arr| {
236    let mut parsed = Vec::new();
237    if let Ok(len) = arr.get_array_length() {
238      for i in 0..len {
239        if let Ok(elem) = arr.get_element::<napi::Unknown>(i)
240          && let Ok(pattern) = parse_js_pattern_from_unknown(&env, elem)
241        {
242          parsed.push(pattern);
243        }
244      }
245    }
246    if parsed.is_empty() {
247      None
248    } else {
249      Some(parsed)
250    }
251  });
252
253  Ok(utils::should_transform_file(
254    &file_path,
255    &include_patterns,
256    &exclude_patterns,
257  ))
258}
259
260/// Parse a JS value (string or RegExp) from an Unknown value
261fn parse_js_pattern_from_unknown(_env: &Env, value: napi::Unknown) -> Result<PathFilterUnion> {
262  // Check if it's an object
263  if value.get_type()? == ValueType::Object {
264    // Try to cast to object
265    if let Ok(obj) = unsafe { value.cast::<napi::JsObject>() } {
266      // Check if it's a RegExp by trying to get 'source' and 'flags' properties
267      if let (Ok(source), Ok(flags)) = (
268        obj.get_named_property::<napi::JsString>("source"),
269        obj.get_named_property::<napi::JsString>("flags"),
270      ) {
271        // It's a RegExp object - convert JS flags to inline modifiers
272        let source_str = source.into_utf8()?.as_str()?.to_owned();
273        let flags_str = flags.into_utf8()?.as_str()?.to_owned();
274
275        // Convert JavaScript flags to regex inline modifiers
276        // Note: 'g' (global) and 'y' (sticky) are not relevant for single-string matching
277        let mut inline_flags = String::new();
278        if flags_str.contains('i') {
279          inline_flags.push('i'); // case insensitive
280        }
281        if flags_str.contains('m') {
282          inline_flags.push('m'); // multiline
283        }
284        if flags_str.contains('s') {
285          inline_flags.push('s'); // dotAll
286        }
287
288        // Prepend inline flags if any exist
289        let pattern = if !inline_flags.is_empty() {
290          format!("(?{}){}", inline_flags, source_str)
291        } else {
292          source_str
293        };
294
295        return Ok(PathFilterUnion::Regex(pattern));
296      }
297
298      // Not a RegExp, try to get it as a string through casting
299      if let Ok(str_val) = unsafe { value.cast::<napi::JsString>() } {
300        let pattern_str = str_val.into_utf8()?.as_str()?.to_owned();
301        return Ok(PathFilterUnion::from_string(&pattern_str));
302      }
303    }
304  } else if value.get_type()? == ValueType::String {
305    // It's already a string, try to cast it
306    if let Ok(str_val) = unsafe { value.cast::<napi::JsString>() } {
307      let pattern_str = str_val.into_utf8()?.as_str()?.to_owned();
308      return Ok(PathFilterUnion::from_string(&pattern_str));
309    }
310  }
311
312  Err(napi::Error::from_reason(
313    "Invalid pattern: must be string or RegExp",
314  ))
315}
316
317#[napi]
318pub fn normalize_rs_options(options: StyleXOptions) -> Result<StyleXOptions> {
319  let normalized_options = StyleXOptions {
320    dev: options
321      .dev
322      .or_else(|| env::var("NODE_ENV").ok().map(|env| env == "development")),
323    enable_font_size_px_to_rem: options.enable_font_size_px_to_rem.or(Some(false)),
324    enable_minified_keys: options.enable_minified_keys.or(Some(true)),
325    runtime_injection: options
326      .runtime_injection
327      .or(Some(RuntimeInjectionUnion::Boolean(false))),
328    treeshake_compensation: options.treeshake_compensation.or(Some(false)),
329    import_sources: options.import_sources.or(Some(vec![
330      ImportSourceUnion::Regular("stylex".to_string()),
331      ImportSourceUnion::Regular("@stylexjs/stylex".to_string()),
332    ])),
333    unstable_module_resolution: options.unstable_module_resolution.or_else(|| {
334      Some(StyleXModuleResolution {
335        r#type: "commonJS".to_string(),
336        root_dir: None,
337        theme_file_extension: None,
338      })
339    }),
340    enable_inlined_conditional_merge: options.enable_inlined_conditional_merge.or(Some(true)),
341    enable_logical_styles_polyfill: options.enable_logical_styles_polyfill.or(Some(false)),
342    enable_legacy_value_flipping: options.enable_legacy_value_flipping.or(Some(false)),
343    enable_ltr_rtl_comments: options.enable_ltr_rtl_comments.or(Some(false)),
344    style_resolution: options
345      .style_resolution
346      .or(Some("property-specificity".to_string())),
347    legacy_disable_layers: options.legacy_disable_layers.or(Some(false)),
348    swc_plugins: options.swc_plugins.or(Some(vec![])),
349    use_real_file_for_source: options.use_real_file_for_source.or(Some(true)),
350    enable_media_query_order: options.enable_media_query_order.or(Some(true)),
351    enable_debug_class_names: options.enable_debug_class_names.or(Some(false)),
352    property_validation_mode: options
353      .property_validation_mode
354      .or(Some(PropertyValidationMode::Silent)),
355    ..options
356  };
357
358  // Validate styleResolution if provided
359  if let Some(ref style_resolution) = normalized_options.style_resolution {
360    // Try to parse it to validate
361    serde_plain::from_str::<StyleResolution>(style_resolution)
362      .map_err(|e| napi::Error::from_reason(format!("Failed to parse style resolution: {}", e)))?;
363  }
364
365  Ok(normalized_options)
366}