mod analyzer;
mod cli;
mod file_loader;
mod parser;
mod reporter;
mod rules;
mod utils;
use std::fs;
use std::time::Instant;
use clap::Parser as ClapParser;
use oxc_allocator::Allocator;
use rayon::prelude::*;
use crate::{
analyzer::analyze,
cli::{Cli, OutputFormat},
file_loader::collect_files,
parser::parse_file,
reporter::{report_html, report_json, report_text},
rules::Issue,
};
fn main() {
let cli = Cli::parse();
let start = Instant::now();
let files = collect_files(&cli.path, cli.include_tests);
if files.is_empty() {
eprintln!("No JS/TS/JSX files found under '{}'.", cli.path.display());
std::process::exit(0);
}
let file_count = files.len();
let max_lines = cli.max_component_lines;
let all_issues: Vec<Issue> = files
.par_iter()
.flat_map(|path| {
let source_text = match fs::read_to_string(path) {
Ok(s) => s,
Err(err) => {
eprintln!("Warning: could not read '{}': {err}", path.display());
return vec![];
}
};
let allocator = Allocator::default();
let program = match parse_file(&allocator, path, &source_text) {
Ok(p) => p,
Err(err) => {
eprintln!(
"Warning: failed to parse '{}': {}",
err.file,
err.messages.join("; ")
);
return vec![];
}
};
analyze(&program, &source_text, path, max_lines)
})
.collect();
let issue_count = match cli.format {
OutputFormat::Text => {
let count = report_text(&all_issues);
if let Some(ref out_path) = cli.output {
let _ = fs::write(out_path, ""); }
count
}
OutputFormat::Json => {
let count = report_json(&all_issues);
reporter::print_summary(&all_issues);
count
}
OutputFormat::Html => {
let html = report_html(&all_issues, &cli.path, file_count);
let out_path = cli
.output
.clone()
.unwrap_or_else(|| std::path::PathBuf::from("react-perf-report.html"));
match fs::write(&out_path, &html) {
Ok(_) => {
eprintln!("✅ HTML report written to: {}", out_path.display());
}
Err(e) => {
eprintln!("Error writing HTML report to '{}': {e}", out_path.display());
std::process::exit(2);
}
}
all_issues.len()
}
};
let elapsed = start.elapsed();
let elapsed_str = if elapsed.as_secs() >= 1 {
format!("{:.2}s", elapsed.as_secs_f64())
} else {
format!("{}ms", elapsed.as_millis())
};
eprintln!(
"\nScanned {file_count} file(s){} in {elapsed_str}.",
if issue_count > 0 {
format!(", found {issue_count} issue(s)")
} else {
String::new()
}
);
std::process::exit(if issue_count > 0 { 1 } else { 0 });
}