use clap::{Parser as ClapParser, Subcommand};
use std::fs;
use std::path::PathBuf;
use reluxscript::{Lexer, Parser, analyze_with_base_dir, TokenRewriter};
#[cfg(feature = "codegen")]
use reluxscript::{generate, Target, lower};
#[derive(ClapParser)]
#[command(name = "reluxscript")]
#[command(about = "ReluxScript compiler - compile to Babel and SWC plugins")]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
New {
name: String,
},
Lex {
file: PathBuf,
},
Parse {
file: PathBuf,
},
Check {
file: PathBuf,
#[arg(long, default_value = "true")]
autofix: bool,
},
#[cfg(feature = "codegen")]
Build {
file: PathBuf,
#[arg(short, long, default_value = "both")]
target: String,
#[arg(short, long, default_value = "dist")]
output: PathBuf,
#[arg(long, default_value = "true")]
autofix: bool,
#[arg(long)]
dump_decorated_ast: bool,
#[arg(long)]
dump_rewritten_ast: bool,
},
#[cfg(feature = "codegen")]
BuildModule {
file: PathBuf,
#[arg(short, long, default_value = "swc")]
target: String,
#[arg(short, long, default_value = "dist")]
output: PathBuf,
#[arg(long)]
dump_ast: bool,
},
Fix {
files: Vec<PathBuf>,
#[arg(long)]
dry_run: bool,
},
#[cfg(feature = "codegen")]
BuildProject {
manifest: PathBuf,
#[arg(short, long, default_value = "swc")]
target: String,
},
}
fn main() {
let cli = <Cli as ClapParser>::parse();
match cli.command {
Commands::New { name } => {
let plugin_file = PathBuf::from(format!("{}.lux", name));
if plugin_file.exists() {
eprintln!("Error: {} already exists", plugin_file.display());
std::process::exit(1);
}
let template = format!(r#"// {name} - A ReluxScript plugin
// Edit this file to implement your AST transformation
plugin {name} {{
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {{
// Example: Remove console.log calls
// if matches!(node.callee, "console.log") {{
// *node = Statement::empty();
// }}
}}
}}
"#, name = name);
if let Err(e) = fs::write(&plugin_file, template) {
eprintln!("Error creating plugin file: {}", e);
std::process::exit(1);
}
println!("Created new plugin: {}", plugin_file.display());
println!("\nNext steps:");
println!(" 1. Edit {} to implement your transformation", plugin_file.display());
println!(" 2. Build to Babel: relux build {} --target babel", plugin_file.display());
println!(" 3. Build to SWC: relux build {} --target swc", plugin_file.display());
}
Commands::Lex { file } => {
let source = match fs::read_to_string(&file) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
let mut lexer = Lexer::new(&source);
let tokens = lexer.tokenize();
println!("Tokens for {:?}:", file);
println!("{:-<60}", "");
for token in &tokens {
println!(
"{:>4}:{:<3} {:?}",
token.span.line,
token.span.column,
token.kind
);
}
println!("{:-<60}", "");
println!("Total tokens: {}", tokens.len());
}
Commands::Parse { file } => {
let source = match fs::read_to_string(&file) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
let mut lexer = Lexer::new(&source);
let tokens = lexer.tokenize();
let mut parser = Parser::new_with_source(tokens, source.clone());
match parser.parse() {
Ok(program) => {
println!("Successfully parsed {:?}", file);
println!("{:-<60}", "");
println!("{:#?}", program);
}
Err(e) => {
eprintln!("Parse error at {}:{}: {}", e.span.line, e.span.column, e.message);
std::process::exit(1);
}
}
}
Commands::Check { file, autofix } => {
let source = match fs::read_to_string(&file) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
let mut lexer = Lexer::new(&source);
let mut tokens = lexer.tokenize();
if autofix {
let rewriter = TokenRewriter::new(tokens);
let (fixed_tokens, fixes_applied) = rewriter.rewrite();
tokens = fixed_tokens;
if fixes_applied > 0 {
println!("Autofix: Applied {} fix(es)", fixes_applied);
}
}
let mut parser = Parser::new_with_source(tokens, source.clone());
let mut program = match parser.parse() {
Ok(p) => p,
Err(e) => {
eprintln!("Parse error at {}:{}: {}", e.span.line, e.span.column, e.message);
std::process::exit(1);
}
};
lower(&mut program);
let base_dir = file.parent().unwrap_or_else(|| std::path::Path::new(".")).to_path_buf();
let result = analyze_with_base_dir(&program, base_dir);
for error in &result.errors {
eprintln!(
"error[{}]: {} at {}:{}",
error.code, error.message, error.span.line, error.span.column
);
if let Some(ref hint) = error.hint {
eprintln!(" help: {}", hint);
}
}
for warning in &result.warnings {
eprintln!(
"warning[{}]: {} at {}:{}",
warning.code, warning.message, warning.span.line, warning.span.column
);
if let Some(ref hint) = warning.hint {
eprintln!(" help: {}", hint);
}
}
if result.errors.is_empty() {
println!("Check passed: {:?}", file);
if !result.warnings.is_empty() {
println!(" {} warning(s)", result.warnings.len());
}
} else {
eprintln!("Check failed: {} error(s)", result.errors.len());
std::process::exit(1);
}
}
#[cfg(feature = "codegen")]
Commands::Build { file, target, output, autofix, dump_decorated_ast, dump_rewritten_ast } => {
let source = match fs::read_to_string(&file) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
let mut lexer = Lexer::new(&source);
let mut tokens = lexer.tokenize();
if autofix {
let rewriter = TokenRewriter::new(tokens);
let (fixed_tokens, fixes_applied) = rewriter.rewrite();
tokens = fixed_tokens;
if fixes_applied > 0 {
println!("Autofix: Applied {} fix(es)", fixes_applied);
}
}
let mut parser = Parser::new_with_source(tokens, source.clone());
let mut program = match parser.parse() {
Ok(p) => p,
Err(e) => {
eprintln!("Parse error at {}:{}: {}", e.span.line, e.span.column, e.message);
std::process::exit(1);
}
};
lower(&mut program);
let base_dir = file.parent().unwrap_or_else(|| std::path::Path::new(".")).to_path_buf();
let result = analyze_with_base_dir(&program, base_dir.clone());
if !result.errors.is_empty() {
for error in &result.errors {
eprintln!(
"error[{}]: {} at {}:{}",
error.code, error.message, error.span.line, error.span.column
);
}
eprintln!("Build failed: {} error(s)", result.errors.len());
std::process::exit(1);
}
let target_enum = match target.as_str() {
"babel" => Target::Babel,
"swc" => Target::Swc,
"both" => Target::Both,
_ => {
eprintln!("Unknown target: {}. Use 'babel', 'swc', or 'both'", target);
std::process::exit(1);
}
};
if dump_decorated_ast {
if target_enum == Target::Babel {
eprintln!("Error: --dump-decorated-ast only works with --target swc");
std::process::exit(1);
}
use reluxscript::SwcDecorator;
let mut decorator = SwcDecorator::with_semantic_types(result.type_env);
let decorated = decorator.decorate_program(&program);
println!("\n=== DECORATED AST FOR SWC (BEFORE REWRITING) ===");
println!("{:#?}", decorated);
println!("=== END DECORATED AST ===\n");
return;
}
if dump_rewritten_ast {
if target_enum == Target::Babel {
eprintln!("Error: --dump-rewritten-ast only works with --target swc");
std::process::exit(1);
}
use reluxscript::{SwcDecorator, SwcRewriter};
let mut decorator = SwcDecorator::with_semantic_types(result.type_env);
let decorated = decorator.decorate_program(&program);
let mut rewriter = SwcRewriter::new();
let rewritten = rewriter.rewrite_program(decorated);
println!("\n=== REWRITTEN AST FOR SWC (AFTER PATTERN DESUGARING) ===");
println!("{:#?}", rewritten);
println!("=== END REWRITTEN AST ===\n");
return;
}
let generated = reluxscript::codegen::generate_with_types_and_base_dir(
&program,
result.type_env.clone(),
target_enum,
base_dir.clone(),
);
if let Err(e) = fs::create_dir_all(&output) {
eprintln!("Error creating output directory: {}", e);
std::process::exit(1);
}
if let Some(babel_code) = generated.babel {
let babel_path = output.join("index.js");
if let Err(e) = fs::write(&babel_path, babel_code) {
eprintln!("Error writing Babel output: {}", e);
std::process::exit(1);
}
println!("Generated Babel plugin: {:?}", babel_path);
let node_check = std::process::Command::new("node")
.arg("--check")
.arg(&babel_path)
.output();
match node_check {
Ok(output) if !output.status.success() => {
eprintln!("\n[VALIDATION ERROR] Generated Babel plugin has syntax errors:");
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
eprintln!("\nCodegen produced invalid JavaScript. This is a compiler bug.");
std::process::exit(1);
}
Ok(_) => {
println!("✓ Babel output validated successfully");
}
Err(_) => {
eprintln!("Warning: Could not validate JS syntax (node not found)");
}
}
}
if let Some(swc_code) = generated.swc {
let swc_path = output.join("lib.rs");
if let Err(e) = fs::write(&swc_path, &swc_code) {
eprintln!("Error writing SWC output: {}", e);
std::process::exit(1);
}
println!("Generated SWC plugin: {:?}", swc_path);
for (module_name, module_code, _imports, _is_transitive) in &generated.swc_modules {
let module_path = output.join(format!("{}.rs", module_name));
if let Err(e) = fs::write(&module_path, module_code) {
eprintln!("Error writing module '{}': {}", module_name, e);
std::process::exit(1);
}
println!("Generated module: {:?}", module_path);
}
let cargo_toml_path = output.join("Cargo.toml");
let needs_cargo_toml = !cargo_toml_path.exists();
if needs_cargo_toml {
let cargo_toml_content = r#"[package]
name = "swc-plugin-temp"
version = "0.1.0"
edition = "2021"
[lib]
path = "lib.rs"
crate-type = ["cdylib", "rlib"]
[dependencies]
swc_common = "17.0.1"
swc_ecma_ast = "18.0.0"
swc_ecma_visit = "18.0.1"
swc_ecma_parser = "27.0.3"
swc_ecma_codegen = "20.0.0"
regex = "1.11.1"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
"#;
if let Err(e) = fs::write(&cargo_toml_path, cargo_toml_content) {
eprintln!("Warning: Could not create Cargo.toml for validation: {}", e);
}
}
let cargo_check = std::process::Command::new("cargo")
.arg("check")
.arg("--manifest-path")
.arg(&cargo_toml_path)
.arg("--lib")
.env("CFLAGS", "")
.env("CXXFLAGS", "")
.env("CC", "")
.env("CXX", "")
.output();
match cargo_check {
Ok(output) if !output.status.success() => {
eprintln!("\n[VALIDATION ERROR] Generated SWC plugin has compilation errors:");
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
eprintln!("\nCodegen produced invalid Rust code. This is a compiler bug.");
if needs_cargo_toml {
let _ = fs::remove_file(&cargo_toml_path);
}
std::process::exit(1);
}
Ok(_) => {
println!("✓ SWC output validated successfully");
if needs_cargo_toml {
let _ = fs::remove_file(&cargo_toml_path);
}
}
Err(_) => {
eprintln!("Warning: Could not validate Rust syntax (cargo not found)");
if needs_cargo_toml {
let _ = fs::remove_file(&cargo_toml_path);
}
}
}
}
println!("Build complete!");
}
#[cfg(feature = "codegen")]
Commands::BuildModule { file, target, output, dump_ast } => {
let source = match fs::read_to_string(&file) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
let mut lexer = Lexer::new(&source);
let tokens = lexer.tokenize();
let mut parser = Parser::new_with_source(tokens, source.clone());
let mut program = match parser.parse() {
Ok(p) => p,
Err(e) => {
eprintln!("Parse error at {}:{}: {}", e.span.line, e.span.column, e.message);
std::process::exit(1);
}
};
lower(&mut program);
let base_dir = file.parent().unwrap_or_else(|| std::path::Path::new(".")).to_path_buf();
let result = analyze_with_base_dir(&program, base_dir.clone());
if !result.errors.is_empty() {
for error in &result.errors {
eprintln!(
"error[{}]: {} at {}:{}",
error.code, error.message, error.span.line, error.span.column
);
}
eprintln!("Build failed: {} error(s)", result.errors.len());
std::process::exit(1);
}
if target != "swc" {
eprintln!("Error: build-module currently only supports --target swc");
std::process::exit(1);
}
use reluxscript::{SwcDecorator, SwcRewriter};
use reluxscript::codegen::SwcEmitter;
let mut decorator = SwcDecorator::with_semantic_types(result.type_env);
let decorated = decorator.decorate_program(&program);
if dump_ast {
println!("\n=== DECORATED MODULE AST ===");
println!("{:#?}", decorated);
println!("=== END DECORATED AST ===\n");
return;
}
let mut rewriter = SwcRewriter::new();
let rewritten = rewriter.rewrite_program(decorated);
let mut emitter = SwcEmitter::with_base_dir(base_dir.clone());
let swc_code = emitter.emit_program(&rewritten);
if let Err(e) = fs::create_dir_all(&output) {
eprintln!("Error creating output directory: {}", e);
std::process::exit(1);
}
let swc_path = output.join("lib.rs");
if let Err(e) = fs::write(&swc_path, &swc_code) {
eprintln!("Error writing SWC output: {}", e);
std::process::exit(1);
}
println!("Generated SWC module: {:?}", swc_path);
for (module_name, module_code, _imports, _is_transitive) in emitter.get_imported_modules() {
let module_path = output.join(format!("{}.rs", module_name));
if let Err(e) = fs::write(&module_path, module_code) {
eprintln!("Error writing module file: {}", e);
std::process::exit(1);
}
println!("Generated module dependency: {:?}", module_path);
}
let module_name = file.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("module")
.to_string();
let manifest = reluxscript::luxon::extract_manifest_with_base_dir(&program, module_name, &base_dir);
let luxon_path = output.join("lib.luxon");
if let Err(e) = manifest.save(&luxon_path) {
eprintln!("Warning: Failed to write .luxon manifest: {}", e);
} else {
println!("Generated manifest: {:?}", luxon_path);
}
println!("Build complete!");
}
Commands::Fix { files, dry_run } => {
let mut total_fixes = 0;
let mut files_changed = 0;
for file in &files {
let source = match fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading {:?}: {}", file, e);
continue;
}
};
let mut lexer = Lexer::new(&source);
let tokens = lexer.tokenize();
let rewriter = TokenRewriter::new(tokens);
let (fixed_tokens, fixes_applied) = rewriter.rewrite();
if fixes_applied == 0 {
if files.len() == 1 {
println!("No fixes needed for {:?}", file);
}
continue;
}
files_changed += 1;
total_fixes += fixes_applied;
println!("{:?}: {} fix(es) applied", file, fixes_applied);
if !dry_run {
let mut parser = Parser::new_with_source(fixed_tokens, source.clone());
match parser.parse() {
Ok(_) => {
println!(" Warning: File NOT rewritten - token-to-source conversion not yet implemented");
println!(" The fixes would have been applied, but source regeneration is needed");
}
Err(e) => {
eprintln!(" Error: Fix validation failed: {}", e.message);
eprintln!(" File NOT modified");
}
}
} else {
println!(" (dry-run: file not modified)");
}
}
println!("\n{} file(s) processed, {} total fix(es)", files.len(), total_fixes);
if files_changed > 0 {
if dry_run {
println!("Run without --dry-run to apply changes");
} else {
println!("Note: Actual file rewriting requires token-to-source conversion (not yet implemented)");
println!("Use --autofix with check/build commands to apply fixes during compilation");
}
}
}
#[cfg(feature = "codegen")]
Commands::BuildProject { manifest, target } => {
use reluxscript::manifest::LuxManifest;
let manifest_path = manifest.clone();
let manifest_dir = manifest_path.parent().unwrap_or_else(|| std::path::Path::new("."));
let lux_manifest = match LuxManifest::load(&manifest_path) {
Ok(m) => m,
Err(e) => {
eprintln!("Error loading manifest: {}", e);
std::process::exit(1);
}
};
if let Err(e) = lux_manifest.validate() {
eprintln!("Manifest validation error: {}", e);
std::process::exit(1);
}
let sorted_modules = match lux_manifest.sorted_modules() {
Ok(m) => m,
Err(e) => {
eprintln!("Dependency error: {}", e);
std::process::exit(1);
}
};
println!("Building project from {:?}", manifest_path);
println!("Build order: {}", sorted_modules.iter().map(|m| m.name.as_str()).collect::<Vec<_>>().join(" → "));
for module in &sorted_modules {
let module_path = manifest_dir.join(&module.path);
let output_path = manifest_dir.join(&module.output);
println!("\n[{}] Building module...", module.name);
if let Err(e) = build_module_internal(&module_path, &output_path, &target) {
eprintln!("Error building module '{}': {}", module.name, e);
std::process::exit(1);
}
println!("[{}] ✓ Built successfully", module.name);
}
if let Some(ref plugin) = lux_manifest.plugin {
let plugin_path = manifest_dir.join(&plugin.path);
let output_path = manifest_dir.join(&plugin.output);
println!("\n[plugin] Building plugin...");
if let Err(e) = build_plugin_internal(&plugin_path, &output_path, &target) {
eprintln!("Error building plugin: {}", e);
std::process::exit(1);
}
println!("[plugin] ✓ Built successfully");
}
if let Some(ref writer) = lux_manifest.writer {
let writer_path = manifest_dir.join(&writer.path);
let output_path = manifest_dir.join(&writer.output);
println!("\n[writer] Building writer...");
if let Err(e) = build_plugin_internal(&writer_path, &output_path, &target) {
eprintln!("Error building writer: {}", e);
std::process::exit(1);
}
println!("[writer] ✓ Built successfully");
}
println!("\n✓ Project build complete!");
}
}
}
#[cfg(feature = "codegen")]
fn build_module_internal(file: &std::path::Path, output: &std::path::Path, target: &str) -> Result<(), String> {
use reluxscript::{Lexer, Parser, lower, analyze_with_base_dir};
use reluxscript::codegen::SwcEmitter;
use reluxscript::{SwcDecorator, SwcRewriter};
let source = fs::read_to_string(file)
.map_err(|e| format!("Failed to read file: {}", e))?;
let mut lexer = Lexer::new(&source);
let tokens = lexer.tokenize();
let mut parser = Parser::new_with_source(tokens, source.clone());
let mut program = parser.parse()
.map_err(|e| format!("Parse error at {}:{}: {}", e.span.line, e.span.column, e.message))?;
lower(&mut program);
let base_dir = file.parent().unwrap_or_else(|| std::path::Path::new(".")).to_path_buf();
let result = analyze_with_base_dir(&program, base_dir.clone());
if !result.errors.is_empty() {
let errors: Vec<_> = result.errors.iter()
.map(|e| format!("{}:{}: {}", e.span.line, e.span.column, e.message))
.collect();
return Err(format!("Semantic errors:\n{}", errors.join("\n")));
}
if target != "swc" {
return Err("build-module currently only supports --target swc".to_string());
}
let mut decorator = SwcDecorator::with_semantic_types(result.type_env);
let decorated = decorator.decorate_program(&program);
let mut rewriter = SwcRewriter::new();
let rewritten = rewriter.rewrite_program(decorated);
let mut emitter = SwcEmitter::with_base_dir(base_dir.clone());
let swc_code = emitter.emit_program(&rewritten);
fs::create_dir_all(output)
.map_err(|e| format!("Failed to create output directory: {}", e))?;
let swc_path = output.join("lib.rs");
fs::write(&swc_path, &swc_code)
.map_err(|e| format!("Failed to write output: {}", e))?;
for (module_name, module_code, _imports, _is_transitive) in emitter.get_imported_modules() {
let module_path = output.join(format!("{}.rs", module_name));
fs::write(&module_path, module_code)
.map_err(|e| format!("Failed to write module '{}': {}", module_name, e))?;
}
let module_name = file.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("module")
.to_string();
let manifest = reluxscript::luxon::extract_manifest_with_base_dir(&program, module_name, &base_dir);
let luxon_path = output.join("lib.luxon");
if let Err(e) = manifest.save(&luxon_path) {
eprintln!("Warning: Failed to write .luxon manifest: {}", e);
}
Ok(())
}
#[cfg(feature = "codegen")]
fn build_plugin_internal(file: &std::path::Path, output: &std::path::Path, target: &str) -> Result<(), String> {
use reluxscript::{Lexer, Parser, lower, analyze_with_base_dir};
use reluxscript::codegen::{BabelGenerator, SwcEmitter, generate_with_types_and_base_dir};
use reluxscript::{SwcDecorator, SwcRewriter, Target};
let source = fs::read_to_string(file)
.map_err(|e| format!("Failed to read file: {}", e))?;
let mut lexer = Lexer::new(&source);
let tokens = lexer.tokenize();
let mut parser = Parser::new_with_source(tokens, source.clone());
let mut program = parser.parse()
.map_err(|e| format!("Parse error at {}:{}: {}", e.span.line, e.span.column, e.message))?;
lower(&mut program);
let base_dir = file.parent().unwrap_or_else(|| std::path::Path::new(".")).to_path_buf();
let result = analyze_with_base_dir(&program, base_dir.clone());
if !result.errors.is_empty() {
let errors: Vec<_> = result.errors.iter()
.map(|e| format!("{}:{}: {}", e.span.line, e.span.column, e.message))
.collect();
return Err(format!("Semantic errors:\n{}", errors.join("\n")));
}
fs::create_dir_all(output)
.map_err(|e| format!("Failed to create output directory: {}", e))?;
let build_target = match target {
"babel" => Target::Babel,
"swc" => Target::Swc,
"both" => Target::Both,
_ => return Err(format!("Unknown target: {}", target)),
};
let generated = generate_with_types_and_base_dir(&program, result.type_env, build_target, base_dir);
if let Some(ref babel) = generated.babel {
let babel_path = output.join("index.js");
fs::write(&babel_path, babel)
.map_err(|e| format!("Failed to write Babel output: {}", e))?;
}
if let Some(ref swc) = generated.swc {
let swc_path = output.join("lib.rs");
fs::write(&swc_path, swc)
.map_err(|e| format!("Failed to write SWC output: {}", e))?;
for (module_name, module_code, _imports, _is_transitive) in &generated.swc_modules {
let module_path = output.join(format!("{}.rs", module_name));
fs::write(&module_path, module_code)
.map_err(|e| format!("Failed to write module '{}': {}", module_name, e))?;
}
}
Ok(())
}