use oxc::allocator::Allocator;
use oxc::codegen::Codegen;
use oxc::parser::Parser;
use oxc::semantic::SemanticBuilder;
use oxc::span::SourceType;
use oxc::transformer::{TransformOptions, Transformer, TypeScriptOptions};
use std::path::Path;
pub fn transpile(source: &str, path: &Path) -> Result<String, TsError> {
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("tsx"))
{
return Err(TsError::JsxNotSupported);
}
let source_type = SourceType::from_path(path).map_err(|e| TsError::BadExtension {
path: path.display().to_string(),
inner: e.to_string(),
})?;
let allocator = Allocator::default();
let parsed = Parser::new(&allocator, source, source_type).parse();
if !parsed.errors.is_empty() {
return Err(TsError::Parse {
path: path.display().to_string(),
errors: parsed
.errors
.iter()
.map(|e| format!("{e:?}"))
.collect::<Vec<_>>()
.join("\n"),
});
}
let mut program = parsed.program;
let opts = TransformOptions {
typescript: TypeScriptOptions {
only_remove_type_imports: true,
..Default::default()
},
..Default::default()
};
let scoping = SemanticBuilder::new()
.with_excess_capacity(2.0)
.build(&program)
.semantic
.into_scoping();
Transformer::new(&allocator, path, &opts).build_with_scoping(scoping, &mut program);
let codegen = Codegen::new().build(&program);
crate::esm::rewrite_esm_to_cjs(&codegen.code, path).map_err(|e| TsError::Parse {
path: path.display().to_string(),
errors: e,
})
}
pub fn lower_esm_js(source: &str, path: &Path) -> Result<String, TsError> {
crate::esm::rewrite_esm_to_cjs(source, path).map_err(|e| TsError::Parse {
path: path.display().to_string(),
errors: e,
})
}
#[derive(Debug, thiserror::Error)]
pub enum TsError {
#[error(
".tsx (JSX) is not supported by the strip-types transpiler; enable a JSX-aware feature or pre-transpile with an external tool"
)]
JsxNotSupported,
#[error("{path}: TypeScript parse error:\n{errors}")]
Parse { path: String, errors: String },
#[error("{path}: unable to resolve source type from extension ({inner})")]
BadExtension { path: String, inner: String },
}
pub fn is_typescript(path: &Path) -> bool {
matches!(
path.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
.as_deref(),
Some("ts" | "mts" | "cts" | "tsx")
)
}