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, ES module imports, and export statements
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 constructs (import/export) that aren't valid in script context
217    let lines: Vec<&str> = cleaned
218        .lines()
219        .filter(|line| {
220            let trimmed = line.trim();
221            // Filter out import and export statements
222            !trimmed.starts_with("import ")
223                && !trimmed.starts_with("export default ")
224                && !trimmed.starts_with("export ")
225        })
226        .collect();
227    cleaned = lines.join("\n");
228    cleaned
229}
230
231/// Internal transformation function that performs the complete TSX-to-JavaScript pipeline.
232///
233/// This is the core algorithm that transforms TSX/JSX syntax into executable JavaScript code.
234/// The transformation follows these steps:
235///
236/// 1. **Source Type Detection**: Determines the file type (TSX) from a virtual path.
237///    This is necessary because Oxc requires a path to infer source type features.
238///
239/// 2. **Content Wrapping** (optional): If `wrap_content` is true, wraps raw TSX content
240///    in a React component function structure. This is needed when transforming standalone
241///    JSX fragments that aren't already wrapped in a function.
242///
243/// 3. **Parsing**: Uses Oxc parser to convert TSX source code into an Abstract Syntax Tree (AST).
244///    The parser handles TypeScript syntax, JSX elements, and all ECMAScript features.
245///
246/// 4. **Semantic Analysis**: Builds semantic information (symbol tables, scopes, etc.) from the AST.
247///    This enables better transformations by understanding variable bindings and scopes.
248///    Uses `with_excess_capacity(2.0)` to pre-allocate memory and reduce reallocations.
249///
250/// 5. **JSX Transformation**: Applies JSX-to-function-call transformations based on the config.
251///    Converts JSX elements like `<div>Hello</div>` into function calls like `h('div', null, 'Hello')`.
252///    The pragma (e.g., `engine.h` or `h`) is determined by the config.
253///
254/// 6. **Code Generation**: Converts the transformed AST back into JavaScript source code.
255///    Optionally minifies the output if `config.minify` is true.
256///
257/// 7. **Code Cleanup**: Replaces pure annotations (`/* @__PURE__ */`) with a space and removes ES module import statements
258///    (not valid in script execution context).
259///
260/// # Performance Considerations
261///
262/// This function is a hot path in the rendering pipeline. Key optimizations:
263/// - Uses `Cow<str>` to avoid unnecessary allocations when content doesn't need wrapping
264/// - Pre-allocates semantic builder with excess capacity to reduce reallocations
265/// - Reuses the allocator across the transformation pipeline
266///
267/// # Arguments
268/// * `tsx_content` - TSX source code to transform
269/// * `config` - Transformation configuration (JSX pragma, minification, etc.)
270/// * `wrap_content` - If true, wraps content in a component function; if false, assumes it's already a function
271///
272/// # Returns
273/// Transformed JavaScript code or an error if parsing/transformation fails
274fn transform_tsx_internal(
275    tsx_content: &str,
276    config: &TsxTransformConfig,
277    wrap_content: bool,
278) -> Result<String, MdxError> {
279    let allocator = Allocator::default();
280
281    // Determine source type from file path and configure for module mode with decorators
282    const COMPONENT_PATH: &str = "component.tsx";
283    let mut source_type = SourceType::from_path(Path::new(COMPONENT_PATH))
284        .map_err(|e| MdxError::SourceType(e.to_string()))?;
285
286    // Enable module mode to better handle export statements and enable decorators
287    source_type = source_type.with_module(true);
288
289    let path = Path::new(COMPONENT_PATH);
290
291    // Optionally wrap content in a proper React component
292    let content_to_parse: Cow<'_, str> = if wrap_content {
293        Cow::Owned(wrap_in_component(tsx_content))
294    } else {
295        Cow::Borrowed(tsx_content)
296    };
297
298    // Parse TSX source into AST
299    let parser_return = Parser::new(&allocator, &content_to_parse, source_type).parse();
300    validate_parse_result(&parser_return)?;
301
302    let mut program = parser_return.program;
303
304    // Build semantic information for better transformation
305    let semantic_return = SemanticBuilder::new()
306        .with_excess_capacity(2.0)
307        .build(&program);
308
309    // Configure and apply JSX transformations
310    let transform_options = create_transform_options(config);
311    let transform_return = Transformer::new(&allocator, path, &transform_options)
312        .build_with_scoping(semantic_return.semantic.into_scoping(), &mut program);
313    validate_transform_result(&transform_return)?;
314
315    // Generate JavaScript code from transformed AST
316    let codegen_options = CodegenOptions {
317        minify: config.minify,
318        ..Default::default()
319    };
320
321    let code = Codegen::new()
322        .with_options(codegen_options)
323        .build(&program)
324        .code;
325
326    // Clean up the generated code
327    let mut cleaned = cleanup_generated_code(&code);
328
329    // Apply component-to-string transformation if component names are provided
330    // This converts h(ComponentName, ...) to h('ComponentName', ...) in the generated code
331    if let Some(component_names) = config.component_names.as_ref() {
332        if !component_names.is_empty() {
333            let names_set: HashSet<&str> = component_names.iter().map(|s| s.as_str()).collect();
334            cleaned = convert_component_refs_in_ast(&cleaned, &names_set);
335        }
336    }
337
338    Ok(cleaned)
339}
340
341/// Transforms a full component function definition (already wrapped)
342///
343/// # Arguments
344/// * `component_code` - Complete component function code with JSX
345///
346/// # Returns
347/// Generated JavaScript code or an error
348pub fn transform_component_function(component_code: &str) -> Result<String, MdxError> {
349    transform_tsx_internal(component_code, &TsxTransformConfig::default(), false)
350}
351
352/// Strips export statements from component code
353///
354/// Removes `export default` and `export` from the beginning of component code
355/// to make it compatible with the TSX parser
356fn strip_export_statements(code: &str) -> String {
357    let trimmed = code.trim();
358
359    // Handle "export default function" or "export default ..."
360    if let Some(rest) = trimmed.strip_prefix("export default ") {
361        return rest.to_string();
362    }
363
364    // Handle "export function" or "export const/let/var"
365    if let Some(rest) = trimmed.strip_prefix("export ") {
366        return rest.to_string();
367    }
368
369    code.to_string()
370}
371
372/// Intelligently transforms component code (detects if it's raw JSX or a function)
373///
374/// # Arguments
375/// * `code` - Component code (either raw JSX or a complete function)
376///
377/// # Returns
378/// Generated JavaScript code or an error
379pub fn transform_component_code(code: &str) -> Result<String, MdxError> {
380    // First, strip any export statements
381    let code_without_exports = strip_export_statements(code);
382    let trimmed = code_without_exports.trim();
383
384    // Check if it's already a function definition
385    let is_function = trimmed.starts_with("function")
386        || (trimmed.starts_with('(') && trimmed.contains("=>"))
387        || trimmed.starts_with("const ")
388        || trimmed.starts_with("let ")
389        || trimmed.starts_with("var ");
390
391    if is_function {
392        // It's a function, transform without wrapping
393        transform_component_function(&code_without_exports)
394    } else {
395        // It's raw JSX, use the normal transformer that wraps it
396        transform_tsx_to_js(&code_without_exports)
397    }
398}