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 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 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 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 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
260fn parse_js_pattern_from_unknown(_env: &Env, value: napi::Unknown) -> Result<PathFilterUnion> {
262 if value.get_type()? == ValueType::Object {
264 if let Ok(obj) = unsafe { value.cast::<napi::JsObject>() } {
266 if let (Ok(source), Ok(flags)) = (
268 obj.get_named_property::<napi::JsString>("source"),
269 obj.get_named_property::<napi::JsString>("flags"),
270 ) {
271 let source_str = source.into_utf8()?.as_str()?.to_owned();
273 let flags_str = flags.into_utf8()?.as_str()?.to_owned();
274
275 let mut inline_flags = String::new();
278 if flags_str.contains('i') {
279 inline_flags.push('i'); }
281 if flags_str.contains('m') {
282 inline_flags.push('m'); }
284 if flags_str.contains('s') {
285 inline_flags.push('s'); }
287
288 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 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 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 if let Some(ref style_resolution) = normalized_options.style_resolution {
360 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}