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);
use oxc::codegen::CodegenOptions;
let codegen = Codegen::new()
.with_options(CodegenOptions {
source_map_path: Some(path.to_path_buf()),
..Default::default()
})
.build(&program);
let mut code = codegen.code;
if let Some(map) = codegen.map.as_ref() {
code.push_str("\n//# sourceMappingURL=");
code.push_str(&map.to_data_url());
code.push('\n');
}
crate::esm::rewrite_esm_to_cjs(&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")
)
}