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 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 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 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}