Skip to main content

stylex_test_parser/
main.rs

1use clap::Parser;
2use fancy_regex::Regex;
3use swc_compiler_base::{PrintArgs, SourceMapsConfig, parse_js, print};
4use swc_config::is_module::IsModule;
5
6use std::{
7  fmt::Display,
8  fs::{self, read_to_string, write},
9  io,
10  path::{Path, PathBuf},
11  sync::Arc,
12};
13use swc_ecma_parser::Syntax;
14
15use swc_core::{
16  common::{
17    DUMMY_SP, FileName, SourceMap, SyntaxContext,
18    errors::{ColorConfig, Handler},
19  },
20  ecma::{
21    ast::{
22      CallExpr, Decl, EsVersion, Expr, ExprStmt, FnDecl, ModuleItem, Stmt, VarDecl, VarDeclKind,
23      VarDeclarator,
24    },
25    visit::{Fold, FoldWith},
26  },
27};
28use walkdir::WalkDir;
29
30use log::error;
31
32#[derive(Parser)]
33struct Cli {
34  #[clap(
35    short = 'p',
36    long,
37    default_value = "../../../stylex/packages",
38    help = "Absolute or relative path to Stylex package repository.",
39    value_name = "PATH"
40  )]
41  stylex_path: Option<String>,
42}
43
44fn read_all_files(root_path: &Path) -> Result<Vec<PathBuf>, walkdir::Error> {
45  let mut files = Vec::new();
46  for entry in WalkDir::new(root_path)
47    .follow_links(true)
48    .into_iter()
49    .filter_map(Result::ok)
50  {
51    if entry.file_type().is_file() {
52      files.push(entry.path().to_owned());
53    }
54  }
55  Ok(files)
56}
57
58struct TestsTransformer {
59  module_items: Vec<ModuleItem>,
60}
61
62impl Fold for TestsTransformer {
63  fn fold_module_items(&mut self, module_items: Vec<ModuleItem>) -> Vec<ModuleItem> {
64    for item in module_items {
65      match item {
66        ModuleItem::Stmt(Stmt::Expr(ExprStmt { expr, .. })) => {
67          if matches!(expr.as_ref(), Expr::Call(_) | Expr::Ident(_) | Expr::Fn(_)) {
68            expr.fold_with(self);
69          }
70        },
71        _ => match item {
72          ModuleItem::Stmt(Stmt::Decl(Decl::Fn(func_decl))) => {
73            func_decl.fold_with(self);
74          },
75          _ => {
76            if let ModuleItem::Stmt(Stmt::Decl(Decl::Var(var))) = item {
77              var.fold_with(self);
78            }
79          },
80        },
81      }
82    }
83
84    self.module_items.clone()
85  }
86  fn fold_call_expr(&mut self, call_expression: CallExpr) -> CallExpr {
87    let black_list = ["require", "jest"];
88
89    let binding = call_expression
90      .callee
91      .as_expr()
92      .and_then(|expr| match *expr.clone() {
93        Expr::Ident(ident) => Some(ident.sym.to_string()),
94        Expr::Member(member) => Some(member.obj.as_ident().unwrap().sym.to_string()),
95        _ => None,
96      })
97      .unwrap_or_default();
98
99    let fn_name = binding.as_str();
100
101    if !black_list.contains(&fn_name) {
102      self
103        .module_items
104        .push(ModuleItem::Stmt(Stmt::from(ExprStmt {
105          span: DUMMY_SP,
106          expr: Box::new(Expr::from(call_expression.clone())),
107        })));
108    }
109    call_expression
110  }
111
112  fn fold_var_declarator(&mut self, var_declarator: VarDeclarator) -> VarDeclarator {
113    if !&var_declarator.init.clone().unwrap().is_call() {
114      self
115        .module_items
116        .push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl {
117          span: DUMMY_SP,
118          kind: VarDeclKind::Var,
119          declare: false,
120          decls: vec![var_declarator.clone()],
121          ctxt: SyntaxContext::empty(),
122        })))));
123    }
124
125    var_declarator
126  }
127
128  fn fold_fn_decl(&mut self, func_decl: FnDecl) -> FnDecl {
129    self
130      .module_items
131      .push(ModuleItem::Stmt(Stmt::Decl(Decl::Fn(func_decl.clone()))));
132
133    func_decl
134  }
135}
136
137struct TypeScriptStripper;
138
139impl Fold for TypeScriptStripper {
140  fn fold_binding_ident(
141    &mut self,
142    mut node: swc_core::ecma::ast::BindingIdent,
143  ) -> swc_core::ecma::ast::BindingIdent {
144    node.type_ann = None;
145
146    node.optional = false;
147
148    node
149  }
150
151  fn fold_function(
152    &mut self,
153    mut node: swc_core::ecma::ast::Function,
154  ) -> swc_core::ecma::ast::Function {
155    node.return_type = None;
156
157    node.fold_children_with(self)
158  }
159
160  fn fold_key_value_prop(
161    &mut self,
162    mut node: swc_core::ecma::ast::KeyValueProp,
163  ) -> swc_core::ecma::ast::KeyValueProp {
164    let new_value = match node.value.as_ref() {
165      Expr::TsAs(ts_as) => Box::new((*ts_as.expr).clone()),
166      _ => node.value.clone(),
167    };
168
169    node.value = new_value;
170
171    node
172  }
173}
174
175fn transform_file(file_path: &Path, dir: &str) -> Result<(), std::io::Error> {
176  let is_snapshot = file_path.to_string_lossy().ends_with(".snap");
177
178  let source_code = read_to_string(file_path)?.replace("{ ... }", "{ }");
179
180  let code = if is_snapshot {
181    source_code
182  } else {
183    let mut transformer = TestsTransformer {
184      module_items: Vec::new(),
185    };
186    // let compiler = Compiler::new(Default::default());
187
188    let cm: Arc<SourceMap> = Default::default();
189
190    let file_path_string = file_path.to_string_lossy().into_owned();
191    let file_name = FileName::Custom(file_path_string);
192
193    let fm = cm.new_source_file(file_name.into(), source_code);
194    // let compiler = Compiler::new(Default::default());
195    let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone()));
196
197    let program = parse_js(
198      cm.clone(),
199      fm,
200      &handler,
201      EsVersion::EsNext,
202      Syntax::Typescript(Default::default()),
203      IsModule::Bool(true),
204      None,
205    )
206    .unwrap();
207
208    // Strip TypeScript annotations first
209    let mut typescript_stripper = TypeScriptStripper;
210    let mut program = program.fold_with(&mut typescript_stripper);
211
212    program = program.fold_with(&mut transformer);
213
214    let transformed_code = print(
215      cm,
216      &program,
217      PrintArgs {
218        source_map: SourceMapsConfig::Bool(false),
219        ..Default::default()
220      },
221    );
222
223    transformed_code.unwrap().code
224  };
225
226  let test_file_name = file_path.file_name().unwrap().to_str().unwrap().to_string();
227
228  write_file(dir, &test_file_name, code)
229}
230
231fn write_file<P: Display, C: AsRef<[u8]>>(
232  dir: P,
233  test_file_name: P,
234  transformed_code: C,
235) -> Result<(), io::Error> {
236  write(format!("{}/{}", dir, test_file_name), transformed_code)
237}
238
239fn create_dir_if_not_exists(dir: &str) -> io::Result<()> {
240  fs::remove_dir_all(dir).unwrap_or_default();
241  fs::create_dir_all(dir)
242}
243
244fn main() {
245  pretty_env_logger::formatted_builder().init();
246  color_backtrace::install();
247
248  let cli = Cli::parse();
249
250  let stylex_dir = cli
251    .stylex_path
252    .expect("Please provide a path to the stylex package");
253
254  let packages = [
255    "babel-plugin",
256    "shared",
257    "stylex",
258    "open-props",
259    "benchmarks",
260  ];
261
262  let re = Regex::new(r"(tests?\.js|tests?\.js\.snap)$").unwrap();
263
264  for package in packages.iter() {
265    let path = format!("{}/{}", stylex_dir, package);
266    let root_path = Path::new(path.as_str());
267
268    let file_paths = match read_all_files(root_path) {
269      Ok(paths) => paths,
270      Err(err) => {
271        error!("Error reading files: {}", err);
272        return;
273      },
274    };
275
276    let file_paths = file_paths
277      .into_iter()
278      .filter(|f| {
279        re.is_match(&f.display().to_string()).unwrap_or_else(|err| {
280          error!(
281            "Error matching regex for '{}': {}. Skipping pattern match.",
282            f.display(),
283            err
284          );
285
286          false
287        }) || f.display().to_string().contains("__tests__")
288          || f.display().to_string().contains("/test/")
289          || f.display().to_string().contains("/tests/")
290      })
291      .collect::<Vec<PathBuf>>();
292
293    let dir = format!("./output/__tests__/{}", package);
294
295    create_dir_if_not_exists(dir.as_str()).unwrap();
296
297    for path in &file_paths {
298      if let Err(err) = transform_file(path, dir.as_str()) {
299        error!("Error transforming file {}: {}", path.display(), err);
300      }
301    }
302  }
303}
304
305#[cfg(test)]
306mod tests {
307  use ctor::ctor;
308
309  #[ctor]
310  fn init_color_backtrace() {
311    pretty_env_logger::formatted_builder().init();
312    color_backtrace::install();
313  }
314}