dinja_core/transform.rs
1//! TSX to JavaScript transformation logic
2//!
3//! This module handles the transformation of TSX/JSX syntax to JavaScript using the Oxc compiler.
4//! It supports various JSX pragmas (engine, React-compatible) and handles component wrapping.
5//!
6//! ## Transformation Process
7//!
8//! 1. Optionally wrap raw TSX in a component function
9//! 2. Parse TSX into an AST using Oxc parser
10//! 3. Build semantic information for better transformation
11//! 4. Apply JSX transformations (convert to function calls)
12//! 5. Generate JavaScript code with optional minification
13//! 6. Clean up generated code (remove pure annotations)
14//!
15//! ## Error Handling
16//!
17//! All transformation errors use `MdxError` for domain-specific error reporting.
18
19use crate::error::MdxError;
20use crate::models::TsxTransformConfig;
21use oxc_allocator::Allocator;
22use oxc_codegen::{Codegen, CodegenOptions};
23use oxc_parser::Parser;
24use oxc_semantic::SemanticBuilder;
25use oxc_span::SourceType;
26use oxc_transformer::{JsxRuntime, TransformOptions, Transformer};
27use std::borrow::Cow;
28use std::cmp::Reverse;
29use std::collections::HashSet;
30use std::path::Path;
31
32/// Base overhead for component wrapper (function declaration, JSX wrapper, etc.)
33const COMPONENT_WRAPPER_OVERHEAD: usize = 100;
34
35/// Wraps TSX content in a React component structure
36///
37/// This function wraps raw TSX/HTML content in a function component that returns
38/// the content wrapped in a Fragment. This is necessary for proper JSX transformation.
39///
40/// # Arguments
41/// * `tsx_content` - Raw TSX/HTML content to wrap
42///
43/// # Returns
44/// A properly formatted React component string
45pub fn wrap_in_component(tsx_content: &str) -> String {
46 // Pre-allocate with estimated capacity to reduce reallocations
47 let estimated_capacity = tsx_content.len() + COMPONENT_WRAPPER_OVERHEAD;
48 let mut result = String::with_capacity(estimated_capacity);
49
50 result.push_str("function View(context = {}) {\n return (\n <>\n");
51
52 // Use iterator combinators for more idiomatic Rust
53 for line in tsx_content.lines() {
54 result.push_str(" ");
55 result.push_str(line);
56 result.push('\n');
57 }
58
59 result.push_str(" </>\n );\n}");
60 result
61}
62
63/// Creates transformation options for JSX processing
64///
65/// # Arguments
66/// * `config` - Configuration for JSX transformation
67///
68/// # Returns
69/// Configured `TransformOptions` for Oxc transformer
70pub fn create_transform_options(config: &TsxTransformConfig) -> TransformOptions {
71 // Start with ES5 target preset
72 let mut options =
73 TransformOptions::from_target("es5").unwrap_or_else(|_| TransformOptions::enable_all());
74 // Clone is necessary here as TransformOptions requires owned String values
75 options.jsx.pragma = Some(config.jsx_pragma.clone());
76 options.jsx.pragma_frag = Some(config.jsx_pragma_frag.clone());
77 options.jsx.runtime = JsxRuntime::Classic;
78 options.jsx.development = false;
79 options.jsx.refresh = None;
80 options
81}
82
83/// Transforms TSX content to JavaScript using the Oxc compiler
84///
85/// This function performs the following steps:
86/// 1. Wraps the TSX content in a component structure
87/// 2. Parses the TSX into an AST
88/// 3. Builds semantic information for the code
89/// 4. Applies JSX transformations (e.g., converting to engine's `h` function)
90/// 5. Generates JavaScript code from the transformed AST
91///
92/// # Arguments
93/// * `tsx_content` - TSX source code to transform
94/// * `config` - Optional transformation configuration (defaults to standard config)
95///
96/// # Returns
97/// Generated JavaScript code or an error
98pub fn transform_tsx_to_js_with_config(
99 tsx_content: &str,
100 config: TsxTransformConfig,
101) -> Result<String, MdxError> {
102 transform_tsx_internal(tsx_content, &config, true)
103}
104
105/// Transforms TSX content to JavaScript using the default configuration
106///
107/// # Arguments
108/// * `tsx_content` - TSX source code to transform
109///
110/// # Returns
111/// Generated JavaScript code or an error
112pub fn transform_tsx_to_js(tsx_content: &str) -> Result<String, MdxError> {
113 transform_tsx_to_js_with_config(tsx_content, TsxTransformConfig::default())
114}
115
116/// Transforms TSX content to JavaScript for final output (uses `h` instead of `engine.h`)
117///
118/// # Arguments
119/// * `tsx_content` - TSX source code to transform
120///
121/// # Returns
122/// Generated JavaScript code or an error
123pub fn transform_tsx_to_js_for_output(tsx_content: &str, minify: bool) -> Result<String, MdxError> {
124 transform_tsx_to_js_with_config(tsx_content, TsxTransformConfig::for_output(minify))
125}
126
127/// Estimated characters per error message including separators
128const ESTIMATED_CHARS_PER_ERROR: usize = 60;
129
130/// Formats a collection of errors into a single error message
131fn format_errors(errors: &[impl std::fmt::Debug]) -> String {
132 if errors.is_empty() {
133 return String::new();
134 }
135
136 // Pre-allocate with estimated capacity to reduce reallocations
137 let estimated_capacity = errors.len() * ESTIMATED_CHARS_PER_ERROR;
138 errors.iter().map(|e| format!("{e:?}")).fold(
139 String::with_capacity(estimated_capacity),
140 |mut acc, e| {
141 if !acc.is_empty() {
142 acc.push_str(", ");
143 }
144 acc.push_str(&e);
145 acc
146 },
147 )
148}
149
150/// Validates and parses TSX content, returning an error if parsing fails
151fn validate_parse_result(parser_return: &oxc_parser::ParserReturn) -> Result<(), MdxError> {
152 if !parser_return.errors.is_empty() {
153 return Err(MdxError::TsxParse(format_errors(&parser_return.errors)));
154 }
155 Ok(())
156}
157
158/// Validates JSX transformation result, returning an error if transformation fails
159fn validate_transform_result(
160 transform_return: &oxc_transformer::TransformerReturn,
161) -> Result<(), MdxError> {
162 if !transform_return.errors.is_empty() {
163 return Err(MdxError::TsxTransform(format_errors(
164 &transform_return.errors,
165 )));
166 }
167 Ok(())
168}
169
170/// Transform that converts component function references to string names in AST
171///
172/// This uses a simple post-processing approach on the generated code since
173/// AST traversal with Oxc requires more complex setup. The string replacement
174/// is safe because we only replace known component names in specific patterns.
175fn convert_component_refs_in_ast(code: &str, component_names: &HashSet<&str>) -> String {
176 if component_names.is_empty() {
177 return code.to_string();
178 }
179
180 let mut result = code.to_string();
181
182 // Sort by length (longest first) to avoid partial matches
183 let mut sorted_names: Vec<&str> = component_names.iter().copied().collect();
184 sorted_names.sort_by_key(|name| Reverse(name.len()));
185
186 for component_name in sorted_names {
187 // Pattern 1: h(ComponentName, -> h('ComponentName',
188 let pattern1 = format!("h({},", component_name);
189 let replacement1 = format!("h('{}',", component_name);
190 result = result.replace(&pattern1, &replacement1);
191
192 // Pattern 2: h(ComponentName) -> h('ComponentName')
193 let pattern2 = format!("h({})", component_name);
194 let replacement2 = format!("h('{}')", component_name);
195 result = result.replace(&pattern2, &replacement2);
196
197 // Pattern 3: engine.h(ComponentName, -> engine.h('ComponentName',
198 let pattern3 = format!("engine.h({},", component_name);
199 let replacement3 = format!("engine.h('{}',", component_name);
200 result = result.replace(&pattern3, &replacement3);
201
202 // Pattern 4: engine.h(ComponentName) -> engine.h('ComponentName')
203 let pattern4 = format!("engine.h({})", component_name);
204 let replacement4 = format!("engine.h('{}')", component_name);
205 result = result.replace(&pattern4, &replacement4);
206 }
207
208 result
209}
210
211/// Cleans up the generated code by removing pure annotations and ES module imports
212fn cleanup_generated_code(code: &str) -> String {
213 let mut cleaned = code.to_string();
214 // Replace pure annotations with a space
215 cleaned = cleaned.replace("/* @__PURE__ */ ", " ");
216 // Remove ES module import statements (they're not valid in script context)
217 // Pattern: import ... from "...";
218 let lines: Vec<&str> = cleaned
219 .lines()
220 .filter(|line| {
221 let trimmed = line.trim();
222 !trimmed.starts_with("import ")
223 })
224 .collect();
225 cleaned = lines.join("\n");
226 cleaned
227}
228
229/// Internal transformation function that performs the complete TSX-to-JavaScript pipeline.
230///
231/// This is the core algorithm that transforms TSX/JSX syntax into executable JavaScript code.
232/// The transformation follows these steps:
233///
234/// 1. **Source Type Detection**: Determines the file type (TSX) from a virtual path.
235/// This is necessary because Oxc requires a path to infer source type features.
236///
237/// 2. **Content Wrapping** (optional): If `wrap_content` is true, wraps raw TSX content
238/// in a React component function structure. This is needed when transforming standalone
239/// JSX fragments that aren't already wrapped in a function.
240///
241/// 3. **Parsing**: Uses Oxc parser to convert TSX source code into an Abstract Syntax Tree (AST).
242/// The parser handles TypeScript syntax, JSX elements, and all ECMAScript features.
243///
244/// 4. **Semantic Analysis**: Builds semantic information (symbol tables, scopes, etc.) from the AST.
245/// This enables better transformations by understanding variable bindings and scopes.
246/// Uses `with_excess_capacity(2.0)` to pre-allocate memory and reduce reallocations.
247///
248/// 5. **JSX Transformation**: Applies JSX-to-function-call transformations based on the config.
249/// Converts JSX elements like `<div>Hello</div>` into function calls like `h('div', null, 'Hello')`.
250/// The pragma (e.g., `engine.h` or `h`) is determined by the config.
251///
252/// 6. **Code Generation**: Converts the transformed AST back into JavaScript source code.
253/// Optionally minifies the output if `config.minify` is true.
254///
255/// 7. **Code Cleanup**: Replaces pure annotations (`/* @__PURE__ */`) with a space and removes ES module import statements
256/// (not valid in script execution context).
257///
258/// # Performance Considerations
259///
260/// This function is a hot path in the rendering pipeline. Key optimizations:
261/// - Uses `Cow<str>` to avoid unnecessary allocations when content doesn't need wrapping
262/// - Pre-allocates semantic builder with excess capacity to reduce reallocations
263/// - Reuses the allocator across the transformation pipeline
264///
265/// # Arguments
266/// * `tsx_content` - TSX source code to transform
267/// * `config` - Transformation configuration (JSX pragma, minification, etc.)
268/// * `wrap_content` - If true, wraps content in a component function; if false, assumes it's already a function
269///
270/// # Returns
271/// Transformed JavaScript code or an error if parsing/transformation fails
272fn transform_tsx_internal(
273 tsx_content: &str,
274 config: &TsxTransformConfig,
275 wrap_content: bool,
276) -> Result<String, MdxError> {
277 let allocator = Allocator::default();
278
279 // Determine source type from file path
280 const COMPONENT_PATH: &str = "component.tsx";
281 let source_type = SourceType::from_path(Path::new(COMPONENT_PATH))
282 .map_err(|e| MdxError::SourceType(e.to_string()))?;
283
284 let path = Path::new(COMPONENT_PATH);
285
286 // Optionally wrap content in a proper React component
287 let content_to_parse: Cow<'_, str> = if wrap_content {
288 Cow::Owned(wrap_in_component(tsx_content))
289 } else {
290 Cow::Borrowed(tsx_content)
291 };
292
293 // Parse TSX source into AST
294 let parser_return = Parser::new(&allocator, &content_to_parse, source_type).parse();
295 validate_parse_result(&parser_return)?;
296
297 let mut program = parser_return.program;
298
299 // Build semantic information for better transformation
300 let semantic_return = SemanticBuilder::new()
301 .with_excess_capacity(2.0)
302 .build(&program);
303
304 // Configure and apply JSX transformations
305 let transform_options = create_transform_options(config);
306 let transform_return = Transformer::new(&allocator, path, &transform_options)
307 .build_with_scoping(semantic_return.semantic.into_scoping(), &mut program);
308 validate_transform_result(&transform_return)?;
309
310 // Generate JavaScript code from transformed AST
311 let codegen_options = CodegenOptions {
312 minify: config.minify,
313 ..Default::default()
314 };
315
316 let code = Codegen::new()
317 .with_options(codegen_options)
318 .build(&program)
319 .code;
320
321 // Clean up the generated code
322 let mut cleaned = cleanup_generated_code(&code);
323
324 // Apply component-to-string transformation if component names are provided
325 // This converts h(ComponentName, ...) to h('ComponentName', ...) in the generated code
326 if let Some(component_names) = config.component_names.as_ref() {
327 if !component_names.is_empty() {
328 let names_set: HashSet<&str> = component_names.iter().map(|s| s.as_str()).collect();
329 cleaned = convert_component_refs_in_ast(&cleaned, &names_set);
330 }
331 }
332
333 Ok(cleaned)
334}
335
336/// Transforms a full component function definition (already wrapped)
337///
338/// # Arguments
339/// * `component_code` - Complete component function code with JSX
340///
341/// # Returns
342/// Generated JavaScript code or an error
343pub fn transform_component_function(component_code: &str) -> Result<String, MdxError> {
344 transform_tsx_internal(component_code, &TsxTransformConfig::default(), false)
345}
346
347/// Intelligently transforms component code (detects if it's raw JSX or a function)
348///
349/// # Arguments
350/// * `code` - Component code (either raw JSX or a complete function)
351///
352/// # Returns
353/// Generated JavaScript code or an error
354pub fn transform_component_code(code: &str) -> Result<String, MdxError> {
355 let trimmed = code.trim();
356
357 // Check if it's already a function definition
358 let is_function = trimmed.starts_with("function")
359 || (trimmed.starts_with('(') && trimmed.contains("=>"))
360 || trimmed.starts_with("const ")
361 || trimmed.starts_with("let ")
362 || trimmed.starts_with("var ");
363
364 if is_function {
365 // It's a function, transform without wrapping
366 transform_component_function(code)
367 } else {
368 // It's raw JSX, use the normal transformer that wraps it
369 transform_tsx_to_js(code)
370 }
371}