use anyhow::{Context, Result};
use std::fs;
use std::mem;
use std::path::{Path, PathBuf};
use oxc::codegen::{CodegenOptions, CodegenReturn};
use oxc::diagnostics::OxcDiagnostic;
use oxc::span::SourceType;
use oxc::transformer::{TransformOptions, TypeScriptOptions};
use oxc::CompilerInterface;
struct CliCompiler {
transform_options: TransformOptions,
printed: String,
errors: Vec<OxcDiagnostic>,
}
impl CliCompiler {
fn new() -> Self {
let typescript = TypeScriptOptions {
only_remove_type_imports: false,
allow_namespaces: true,
allow_declare_fields: true,
remove_class_fields_without_initializer: false,
..Default::default()
};
let transform_options = TransformOptions {
typescript,
..Default::default()
};
Self {
transform_options,
printed: String::new(),
errors: Vec::new(),
}
}
fn execute(
&mut self,
source_text: &str,
source_type: SourceType,
source_path: &Path,
) -> Result<String, Vec<OxcDiagnostic>> {
self.compile(source_text, source_type, source_path);
if self.errors.is_empty() {
Ok(mem::take(&mut self.printed))
} else {
Err(mem::take(&mut self.errors))
}
}
}
impl CompilerInterface for CliCompiler {
fn handle_errors(&mut self, errors: Vec<OxcDiagnostic>) {
self.errors.extend(errors);
}
fn after_codegen(&mut self, ret: CodegenReturn) {
self.printed = ret.code;
}
fn transform_options(&self) -> Option<&TransformOptions> {
Some(&self.transform_options)
}
fn codegen_options(&self) -> Option<CodegenOptions> {
Some(CodegenOptions::default())
}
}
pub fn compile_file(source_path: &Path) -> Result<String> {
let source_text = fs::read_to_string(source_path)
.with_context(|| format!("Failed to read source file: {}", source_path.display()))?;
let source_type = SourceType::from_path(source_path)
.map_err(|e| anyhow::anyhow!("Failed to determine source type: {:?}", e))?;
compile_source(&source_text, source_type, source_path)
}
pub fn compile_source(
source_text: &str,
source_type: SourceType,
source_path: &Path,
) -> Result<String> {
let mut compiler = CliCompiler::new();
match compiler.execute(source_text, source_type, source_path) {
Ok(output) => Ok(output),
Err(errors) => {
let error_messages: Vec<String> = errors
.iter()
.map(|e| format!("{:?}", e))
.collect();
anyhow::bail!("Compilation errors:\n{}", error_messages.join("\n"))
}
}
}
pub fn compile_typescript(src_dir: &Path, out_dir: &Path) -> Result<Vec<PathBuf>> {
let mut outputs = Vec::new();
fs::create_dir_all(out_dir)?;
for entry in walkdir::WalkDir::new(src_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
let path = e.path();
path.is_file()
&& path
.extension()
.map_or(false, |ext| ext == "ts" || ext == "tsx" || ext == "js" || ext == "jsx")
})
{
let source_path = entry.path();
let relative_path = source_path
.strip_prefix(src_dir)
.unwrap_or(source_path);
let mut output_path = out_dir.join(relative_path);
output_path.set_extension("js");
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
println!(" Compiling: {}", relative_path.display());
let output = compile_file(source_path)
.with_context(|| format!("Failed to compile {}", source_path.display()))?;
fs::write(&output_path, &output)?;
outputs.push(output_path);
}
println!(" Compiled {} files with OXC", outputs.len());
Ok(outputs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compile_simple_typescript() {
let source = "const x: number = 42; console.log(x);";
let source_type = SourceType::ts();
let result = compile_source(source, source_type, Path::new("test.ts")).unwrap();
assert!(result.contains("console.log"));
assert!(!result.contains(": number"));
}
#[test]
fn test_compile_tsx() {
let source = "const App = () => <div>Hello</div>;";
let source_type = SourceType::tsx();
let result = compile_source(source, source_type, Path::new("test.tsx")).unwrap();
assert!(!result.contains("<div>"));
}
}