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}