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}