use crate::error::MdxError;
use crate::models::TsxTransformConfig;
use oxc_allocator::Allocator;
use oxc_codegen::{Codegen, CodegenOptions};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_transformer::{JsxRuntime, TransformOptions, Transformer};
use std::borrow::Cow;
use std::cmp::Reverse;
use std::collections::HashSet;
use std::path::Path;
const COMPONENT_WRAPPER_OVERHEAD: usize = 100;
pub fn wrap_in_component(tsx_content: &str) -> String {
let estimated_capacity = tsx_content.len() + COMPONENT_WRAPPER_OVERHEAD;
let mut result = String::with_capacity(estimated_capacity);
result.push_str("function View(context = {}) {\n return (\n <>\n");
for line in tsx_content.lines() {
result.push_str(" ");
result.push_str(line);
result.push('\n');
}
result.push_str(" </>\n );\n}");
result
}
pub fn create_transform_options(config: &TsxTransformConfig) -> TransformOptions {
let mut options =
TransformOptions::from_target("es5").unwrap_or_else(|_| TransformOptions::enable_all());
options.jsx.pragma = Some(config.jsx_pragma.clone());
options.jsx.pragma_frag = Some(config.jsx_pragma_frag.clone());
options.jsx.runtime = JsxRuntime::Classic;
options.jsx.development = false;
options.jsx.refresh = None;
options
}
pub fn transform_tsx_to_js_with_config(
tsx_content: &str,
config: TsxTransformConfig,
) -> Result<String, MdxError> {
transform_tsx_internal(tsx_content, &config, true)
}
pub fn transform_tsx_to_js(tsx_content: &str) -> Result<String, MdxError> {
transform_tsx_to_js_with_config(tsx_content, TsxTransformConfig::default())
}
pub fn transform_tsx_to_js_for_output(tsx_content: &str, minify: bool) -> Result<String, MdxError> {
transform_tsx_to_js_with_config(tsx_content, TsxTransformConfig::for_output(minify))
}
const ESTIMATED_CHARS_PER_ERROR: usize = 60;
fn format_errors(errors: &[impl std::fmt::Debug]) -> String {
if errors.is_empty() {
return String::new();
}
let estimated_capacity = errors.len() * ESTIMATED_CHARS_PER_ERROR;
errors.iter().map(|e| format!("{e:?}")).fold(
String::with_capacity(estimated_capacity),
|mut acc, e| {
if !acc.is_empty() {
acc.push_str(", ");
}
acc.push_str(&e);
acc
},
)
}
fn validate_parse_result(parser_return: &oxc_parser::ParserReturn) -> Result<(), MdxError> {
if !parser_return.errors.is_empty() {
return Err(MdxError::TsxParse(format_errors(&parser_return.errors)));
}
Ok(())
}
fn validate_transform_result(
transform_return: &oxc_transformer::TransformerReturn,
) -> Result<(), MdxError> {
if !transform_return.errors.is_empty() {
return Err(MdxError::TsxTransform(format_errors(
&transform_return.errors,
)));
}
Ok(())
}
fn convert_component_refs_in_ast(code: &str, component_names: &HashSet<&str>) -> String {
if component_names.is_empty() {
return code.to_string();
}
let mut result = code.to_string();
let mut sorted_names: Vec<&str> = component_names.iter().copied().collect();
sorted_names.sort_by_key(|name| Reverse(name.len()));
for component_name in sorted_names {
let pattern1 = format!("h({},", component_name);
let replacement1 = format!("h('{}',", component_name);
result = result.replace(&pattern1, &replacement1);
let pattern2 = format!("h({})", component_name);
let replacement2 = format!("h('{}')", component_name);
result = result.replace(&pattern2, &replacement2);
let pattern3 = format!("engine.h({},", component_name);
let replacement3 = format!("engine.h('{}',", component_name);
result = result.replace(&pattern3, &replacement3);
let pattern4 = format!("engine.h({})", component_name);
let replacement4 = format!("engine.h('{}')", component_name);
result = result.replace(&pattern4, &replacement4);
}
result
}
fn cleanup_generated_code(code: &str) -> String {
let mut cleaned = code.to_string();
cleaned = cleaned.replace("/* @__PURE__ */ ", " ");
let lines: Vec<&str> = cleaned
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.starts_with("import ")
&& !trimmed.starts_with("export default ")
&& !trimmed.starts_with("export ")
})
.collect();
cleaned = lines.join("\n");
cleaned
}
fn transform_tsx_internal(
tsx_content: &str,
config: &TsxTransformConfig,
wrap_content: bool,
) -> Result<String, MdxError> {
let allocator = Allocator::default();
const COMPONENT_PATH: &str = "component.tsx";
let mut source_type = SourceType::from_path(Path::new(COMPONENT_PATH))
.map_err(|e| MdxError::SourceType(e.to_string()))?;
source_type = source_type.with_module(true);
let path = Path::new(COMPONENT_PATH);
let content_to_parse: Cow<'_, str> = if wrap_content {
Cow::Owned(wrap_in_component(tsx_content))
} else {
Cow::Borrowed(tsx_content)
};
let parser_return = Parser::new(&allocator, &content_to_parse, source_type).parse();
validate_parse_result(&parser_return)?;
let mut program = parser_return.program;
let semantic_return = SemanticBuilder::new()
.with_excess_capacity(2.0)
.build(&program);
let transform_options = create_transform_options(config);
let transform_return = Transformer::new(&allocator, path, &transform_options)
.build_with_scoping(semantic_return.semantic.into_scoping(), &mut program);
validate_transform_result(&transform_return)?;
let codegen_options = CodegenOptions {
minify: config.minify,
..Default::default()
};
let code = Codegen::new()
.with_options(codegen_options)
.build(&program)
.code;
let mut cleaned = cleanup_generated_code(&code);
if let Some(component_names) = config.component_names.as_ref() {
if !component_names.is_empty() {
let names_set: HashSet<&str> = component_names.iter().map(|s| s.as_str()).collect();
cleaned = convert_component_refs_in_ast(&cleaned, &names_set);
}
}
Ok(cleaned)
}
pub fn transform_component_function(component_code: &str) -> Result<String, MdxError> {
transform_tsx_internal(component_code, &TsxTransformConfig::default(), false)
}
fn strip_export_statements(code: &str) -> String {
let trimmed = code.trim();
if let Some(rest) = trimmed.strip_prefix("export default ") {
return rest.to_string();
}
if let Some(rest) = trimmed.strip_prefix("export ") {
return rest.to_string();
}
code.to_string()
}
pub fn transform_component_code(code: &str) -> Result<String, MdxError> {
let code_without_exports = strip_export_statements(code);
let trimmed = code_without_exports.trim();
let is_function = trimmed.starts_with("function")
|| (trimmed.starts_with('(') && trimmed.contains("=>"))
|| trimmed.starts_with("const ")
|| trimmed.starts_with("let ")
|| trimmed.starts_with("var ");
if is_function {
transform_component_function(&code_without_exports)
} else {
transform_tsx_to_js(&code_without_exports)
}
}