use anyhow::{bail, Result};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, Default)]
pub enum EsTarget {
Es5,
Es2015,
Es2016,
Es2017,
Es2018,
Es2019,
#[default]
Es2020,
Es2021,
Es2022,
EsNext,
}
#[derive(Debug, Clone, Default)]
pub struct CompileOptions {
pub target: EsTarget,
pub jsx: bool,
pub jsx_automatic_runtime: bool,
pub source_maps: bool,
pub module: bool,
}
pub struct CompileResult {
pub code: String,
pub source_map: Option<String>,
}
pub fn compile_file(_source: &str, _filename: &str, _options: &CompileOptions) -> Result<CompileResult> {
bail!(
"Native SWC compilation not yet available. Use --use-tsc flag for TypeScript compilation.\n\
Native compilation will be enabled once SWC version compatibility is resolved."
)
}
pub fn compile_project(
src_dir: &Path,
out_dir: &Path,
_options: &CompileOptions,
) -> Result<Vec<PathBuf>> {
let mut copied_files = vec![];
copy_ts_files(src_dir, src_dir, out_dir, &mut copied_files)?;
Ok(copied_files)
}
fn copy_ts_files(
root_src: &Path,
current_src: &Path,
out_dir: &Path,
copied_files: &mut Vec<PathBuf>,
) -> Result<()> {
for entry in fs::read_dir(current_src)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if file_name_str.starts_with('.')
|| file_name_str == "node_modules"
|| file_name_str.ends_with(".test.ts")
|| file_name_str.ends_with(".test.tsx")
|| file_name_str.ends_with(".spec.ts")
|| file_name_str.ends_with(".spec.tsx")
|| file_name_str == "__tests__"
{
continue;
}
if path.is_dir() {
let relative_path = path.strip_prefix(root_src)?;
let out_subdir = out_dir.join(relative_path);
fs::create_dir_all(&out_subdir)?;
copy_ts_files(root_src, &path, out_dir, copied_files)?;
} else if path.is_file() {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if matches!(ext, "ts" | "tsx" | "js" | "jsx" | "json" | "css") {
let relative_path = path.strip_prefix(root_src)?;
let out_path = out_dir.join(relative_path);
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&path, &out_path)?;
copied_files.push(out_path);
}
}
}
Ok(())
}
pub fn generate_declarations(
src_dir: &Path,
out_dir: &Path,
) -> Result<Vec<PathBuf>> {
let mut declaration_files = vec![];
generate_declarations_for_directory(src_dir, src_dir, out_dir, &mut declaration_files)?;
Ok(declaration_files)
}
fn generate_declarations_for_directory(
root_src: &Path,
current_src: &Path,
out_dir: &Path,
declaration_files: &mut Vec<PathBuf>,
) -> Result<()> {
for entry in fs::read_dir(current_src)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if file_name_str.starts_with('.')
|| file_name_str == "node_modules"
|| file_name_str.ends_with(".test.ts")
|| file_name_str.ends_with(".test.tsx")
|| file_name_str.ends_with(".spec.ts")
|| file_name_str.ends_with(".spec.tsx")
|| file_name_str == "__tests__"
{
continue;
}
if path.is_dir() {
let relative_path = path.strip_prefix(root_src)?;
let out_subdir = out_dir.join(relative_path);
fs::create_dir_all(&out_subdir)?;
generate_declarations_for_directory(root_src, &path, out_dir, declaration_files)?;
} else if path.is_file() {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if file_name_str.ends_with(".d.ts") {
let relative_path = path.strip_prefix(root_src)?;
let out_path = out_dir.join(relative_path);
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&path, &out_path)?;
declaration_files.push(out_path);
} else if matches!(ext, "ts" | "tsx") {
let relative_path = path.strip_prefix(root_src)?;
let mut out_path = out_dir.join(relative_path);
out_path.set_extension("d.ts");
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
let source = fs::read_to_string(&path)?;
let decl_content = extract_exports_for_declaration(&source);
fs::write(&out_path, decl_content)?;
declaration_files.push(out_path);
}
}
}
Ok(())
}
fn extract_exports_for_declaration(source: &str) -> String {
let mut declarations = vec![];
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("export type ") || trimmed.starts_with("export interface ") {
declarations.push(line.to_string());
continue;
}
if trimmed.starts_with("export const ") {
if let Some(name_end) = trimmed.find(':') {
let name_part = &trimmed[13..name_end].trim();
let type_start = name_end + 1;
if let Some(eq_pos) = trimmed[type_start..].find('=') {
let type_part = trimmed[type_start..type_start + eq_pos].trim();
declarations.push(format!("export declare const {}: {};", name_part, type_part));
}
} else if let Some(eq_pos) = trimmed.find('=') {
let name_part = &trimmed[13..eq_pos].trim();
declarations.push(format!("export declare const {}: any;", name_part));
}
continue;
}
if trimmed.starts_with("export function ") || trimmed.starts_with("export async function ") {
let fn_start = if trimmed.starts_with("export async") { 22 } else { 16 };
if let Some(paren_pos) = trimmed.find('(') {
let fn_name = &trimmed[fn_start..paren_pos].trim();
declarations.push(format!("export declare function {}(...args: any[]): any;", fn_name));
}
continue;
}
if trimmed.starts_with("export {") || trimmed.starts_with("export * from") {
declarations.push(line.to_string());
continue;
}
}
if declarations.is_empty() {
"export {};\n".to_string()
} else {
declarations.join("\n") + "\n"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_exports() {
let source = r#"
export type Foo = string;
export interface Bar { name: string; }
export const greeting: string = "hello";
export function greet(name: string): string { return name; }
export * from './other';
"#;
let decl = extract_exports_for_declaration(source);
assert!(decl.contains("export type Foo"));
assert!(decl.contains("export interface Bar"));
assert!(decl.contains("export declare const greeting"));
assert!(decl.contains("export declare function greet"));
assert!(decl.contains("export * from"));
}
}