1mod jsx_parser;
2mod swc_transformer;
3pub mod debug;
4#[cfg(all(feature = "native-swc", not(target_arch = "wasm32")))]
5mod swc_native;
6
7#[cfg(feature = "wasm")]
8use serde::{Deserialize, Serialize};
9
10pub use debug::{DebugLevel, DebugContext, DebugEntry};
11
12#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum TranspileTarget {
16 Web,
18 Android,
20}
21
22impl Default for TranspileTarget {
23 fn default() -> Self {
24 Self::Web
25 }
26}
27
28pub struct TranspileOptions {
29 pub is_typescript: bool,
30 pub target: TranspileTarget,
32 pub filename: Option<String>,
34 pub to_commonjs: bool,
36 pub source_maps: bool,
38 pub inline_source_map: bool,
40 pub compat_for_jsc: bool,
42 pub debug_level: DebugLevel,
44}
45
46impl Default for TranspileOptions {
47 fn default() -> Self {
48 Self {
49 is_typescript: false,
50 target: TranspileTarget::Web,
51 filename: None,
52 to_commonjs: false,
53 source_maps: false,
54 inline_source_map: false,
55 compat_for_jsc: true,
56 debug_level: DebugLevel::default(),
57 }
58 }
59}
60
61#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
63#[derive(Debug, Clone, PartialEq)]
64pub struct ImportMetadata {
65 pub source: String,
66 pub kind: ImportKind,
67 pub bindings: Vec<ImportBinding>,
68}
69
70#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
71#[derive(Debug, Clone, PartialEq)]
72#[cfg_attr(feature = "wasm", serde(tag = "type", content = "value"))]
73pub enum ImportKind {
74 Builtin,
75 SpecialPackage,
76 Module,
77}
78
79#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
80#[derive(Debug, Clone, PartialEq)]
81pub struct ImportBinding {
82 pub binding_type: ImportBindingType,
83 pub name: String,
84 pub alias: Option<String>,
85}
86
87#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
88#[derive(Debug, Clone, PartialEq)]
89#[cfg_attr(feature = "wasm", serde(tag = "type"))]
90pub enum ImportBindingType {
91 Default,
92 Named,
93 Namespace,
94}
95
96#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
98#[derive(Debug, Clone, PartialEq)]
99pub struct TranspileMetadata {
100 pub imports: Vec<ImportMetadata>,
101 pub has_jsx: bool,
102 pub has_dynamic_import: bool,
103 pub version: String,
104}
105
106pub fn version() -> &'static str {
108 env!("CARGO_PKG_VERSION")
109}
110
111pub fn transpile_jsx_simple(source: &str) -> Result<String, String> {
114 transpile_jsx_with_options(source, &TranspileOptions::default())
115}
116
117pub fn transpile_jsx_with_options(source: &str, opts: &TranspileOptions) -> Result<String, String> {
119 let debug_ctx = DebugContext::new(opts.debug_level);
120
121 debug_ctx.info(format!("Starting transpilation for target: {:?}", opts.target));
122 if let Some(filename) = &opts.filename {
123 debug_ctx.trace(format!("File: {}", filename));
124 }
125 debug_ctx.trace(format!("Options: typescript={}, commonjs={}, maps={}",
126 opts.is_typescript, opts.to_commonjs, opts.source_maps));
127
128 #[cfg(all(feature = "native-swc", not(target_arch = "wasm32")))]
130 if opts.target == TranspileTarget::Android {
131 debug_ctx.trace("Using SWC transpiler for Android target");
132 let mut code = swc_native::transpile_with_swc(source, opts)
133 .map_err(|e| {
134 debug_ctx.error(format!("SWC transpile error: {}", e));
135 format!("SWC transpile failed: {e}")
136 })?;
137
138 debug_ctx.trace("Applying dynamic import transformation");
141 code = jsx_parser::transform_dynamic_imports(&code);
142
143 debug_ctx.info("Transpilation completed successfully");
144 return Ok(code);
145 }
146
147 debug_ctx.trace("Using JSX parser for transpilation");
148 let jsx_output = jsx_parser::transpile_jsx(source, opts).map_err(|e| {
149 debug_ctx.error(format!("JSX parse error: {}", e));
150 e.to_string()
151 })?;
152 debug_ctx.trace("JSX transformation complete");
153
154 if opts.target == TranspileTarget::Android {
157 debug_ctx.trace("Applying ES5 downleveling for Android JavaScriptCore");
158 let downleveled = swc_transformer::downlevel_for_jsc(&jsx_output)
159 .map_err(|e| {
160 debug_ctx.error(format!("ES5 downlevel error: {}", e));
161 format!("ES5 transformation failed: {}", e)
162 })?;
163
164 debug_ctx.trace("Applying dynamic import transformation");
167 let transformed = jsx_parser::transform_dynamic_imports(&downleveled);
168
169 debug_ctx.info("Transpilation completed successfully");
170 return Ok(transformed);
171 }
172
173 debug_ctx.info("Transpilation completed successfully");
174 Ok(jsx_output)
175}
176
177pub fn transform_es6_modules(source: &str) -> String {
181 jsx_parser::transform_es6_modules(source)
182}
183
184#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
186#[derive(Debug, Clone, PartialEq)]
187pub struct StaticImportMetadata {
188 pub module: String,
189 pub imported: Vec<String>,
190 pub is_default: bool,
191 pub is_namespace: bool,
192 pub is_lazy: bool,
193}
194
195pub fn extract_imports(source: &str) -> Vec<StaticImportMetadata> {
198 jsx_parser::extract_imports(source)
199 .into_iter()
200 .map(|m| StaticImportMetadata {
201 module: m.module,
202 imported: m.imported,
203 is_default: m.is_default,
204 is_namespace: m.is_namespace,
205 is_lazy: m.is_lazy,
206 })
207 .collect()
208}
209
210#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
213#[derive(Debug, Clone)]
214pub struct TranspileResult {
215 pub code: String,
216 pub metadata: TranspileMetadata,
217}
218
219pub fn transpile_jsx_with_metadata(source: &str, _filename: Option<&str>, is_typescript: bool) -> Result<TranspileResult, String> {
222 let debug_ctx = DebugContext::new(DebugLevel::default());
223
224 debug_ctx.trace("Extracting metadata from source");
225
226 let has_jsx = source.contains('<') && source.contains('>') &&
228 (source.contains("return") || source.contains("(") || source.contains("<"));
229 debug_ctx.trace(format!("Has JSX: {}", has_jsx));
230
231 let has_dynamic_import = source.contains("import(");
233 debug_ctx.trace(format!("Has dynamic imports: {}", has_dynamic_import));
234
235 let opts = TranspileOptions {
237 is_typescript,
238 target: TranspileTarget::Web,
239 filename: _filename.map(|f| f.to_string()),
240 debug_level: DebugLevel::default(),
241 ..Default::default()
242 };
243 let code = transpile_jsx_with_options(source, &opts)?;
244
245 debug_ctx.trace("Extracting import bindings");
246 let imports = extract_imports_with_bindings(source);
248 debug_ctx.info(format!("Extracted {} imports", imports.len()));
249
250 Ok(TranspileResult {
251 code,
252 metadata: TranspileMetadata {
253 imports,
254 has_jsx,
255 has_dynamic_import,
256 version: version().to_string(),
257 },
258 })
259}
260
261fn extract_imports_with_bindings(source: &str) -> Vec<ImportMetadata> {
263 jsx_parser::extract_imports(source)
264 .into_iter()
265 .map(|m| {
266 let kind = classify_import(&m.module);
267
268 let bindings = if m.is_namespace {
270 m.imported.into_iter().map(|name| {
271 ImportBinding {
272 binding_type: ImportBindingType::Namespace,
273 name,
274 alias: None,
275 }
276 }).collect()
277 } else if m.is_default {
278 m.imported.into_iter().map(|name| {
279 ImportBinding {
280 binding_type: ImportBindingType::Default,
281 name,
282 alias: None,
283 }
284 }).collect()
285 } else {
286 m.imported.into_iter().map(|name| {
288 if name.contains(" as ") {
290 let parts: Vec<&str> = name.split(" as ").collect();
291 ImportBinding {
292 binding_type: ImportBindingType::Named,
293 name: parts[0].trim().to_string(),
294 alias: Some(parts[1].trim().to_string()),
295 }
296 } else {
297 ImportBinding {
298 binding_type: ImportBindingType::Named,
299 name,
300 alias: None,
301 }
302 }
303 }).collect()
304 };
305
306 ImportMetadata {
307 source: m.module,
308 kind,
309 bindings,
310 }
311 })
312 .collect()
313}
314
315fn classify_import(source: &str) -> ImportKind {
317 if source == "react" || source == "react-dom" || source == "react-native" {
318 ImportKind::SpecialPackage
319 } else if source.starts_with("@clevertree/") {
320 ImportKind::SpecialPackage
321 } else if source.starts_with("@") {
322 ImportKind::Module
324 } else if source.starts_with(".") {
325 ImportKind::Module
326 } else if source.contains("/") {
327 ImportKind::Module
328 } else {
329 ImportKind::Module
331 }
332}
333
334#[cfg(feature = "wasm")]
336mod wasm_api;
337
338#[cfg(feature = "android")]
339mod android_jni;
340
341mod ffi;
342pub use ffi::*;
343
344#[cfg(target_vendor = "apple")]
345mod ios_ffi;
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 #[cfg(all(feature = "native-swc", not(target_arch = "wasm32")))]
353 fn swc_native_emits_cjs_and_sourcemap_footer() {
354 let opts = TranspileOptions {
355 is_typescript: false,
356 target: TranspileTarget::Android,
357 filename: Some("map-test.jsx".to_string()),
358 to_commonjs: true,
359 source_maps: true,
360 inline_source_map: true,
361 compat_for_jsc: true,
362 debug_level: Default::default(),
363 };
364
365 let code = r#"
366 export default function Component() {
367 return <div>hello</div>;
368 }
369 "#;
370
371 let output = transpile_jsx_with_options(code, &opts).expect("SWC transpile should succeed");
372 println!("SWC output:\n{}", output);
373 assert!(
374 output.contains("module.exports")
375 || output.contains("exports.default")
376 || output.contains("Object.defineProperty(exports, \"default\"")
377 , "should emit CommonJS exports");
378 assert!(output.contains("__hook_jsx_runtime") || output.contains("jsx"), "should include JSX runtime calls");
379 assert!(output.contains("sourceMappingURL=data:application/json;base64,"), "should include inline source map footer");
380
381 let footer_prefix = "sourceMappingURL=data:application/json;base64,";
383 let footer_start = output.find(footer_prefix).unwrap() + footer_prefix.len();
384 let encoded_map = &output[footer_start..].trim();
385 let decoded_map = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded_map).unwrap();
386 let map_json: serde_json::Value = serde_json::from_slice(&decoded_map).unwrap();
387
388 assert!(map_json["sources"].as_array().unwrap().iter().any(|s| s.as_str() == Some("map-test.jsx")), "should contain filename in sources");
389 assert!(map_json["sourcesContent"].as_array().is_some(), "should contain sourcesContent");
390 assert!(map_json["sourcesContent"].as_array().unwrap()[0].as_str().unwrap().contains("Component"), "sourcesContent should contain original source");
391 }
392
393 #[test]
394 fn test_simple_jsx() {
395 let input = "<div>Hello</div>";
396 let output = transpile_jsx_simple(input).unwrap();
397 assert!(output.contains("__hook_jsx_runtime.jsx"));
398 assert!(output.contains("div"));
399 assert!(output.contains("Hello"));
400 }
401
402 #[test]
403 fn test_jsx_with_props() {
404 let input = r#"<div className="test">Content</div>"#;
405 let output = transpile_jsx_simple(input).unwrap();
406 assert!(output.contains("className"));
407 assert!(output.contains("test"));
408 }
409
410 #[test]
411 fn test_self_closing() {
412 let input = "<img src='test.jpg' />";
413 let output = transpile_jsx_simple(input).unwrap();
414 assert!(output.contains("img"));
415 assert!(output.contains("src"));
416 }
417
418 #[test]
419 fn test_strings_with_reserved_keywords() {
420 let input = r#"<p className="text-xs text-gray-500 mb-2">Select a theme for the application</p>"#;
423 let output = transpile_jsx_simple(input).unwrap();
424
425 assert!(output.contains("className"));
427 assert!(output.contains("text-xs text-gray-500 mb-2"));
428
429 assert!(output.contains("Select a theme for the application"));
431
432 assert!(output.contains("__hook_jsx_runtime.jsx"));
434 }
435
436 #[test]
437 fn test_string_with_jsx_like_content() {
438 let input = r#"<div>{"<div>test</div>"}</div>"#;
440 let output = transpile_jsx_simple(input).unwrap();
441
442 assert!(output.contains("__hook_jsx_runtime.jsx"));
444 assert!(output.contains("div"));
445
446 assert!(output.contains("<div>test</div>"));
448 }
449
450 #[test]
451 fn test_multiple_class_names() {
452 let input = r#"<button className="px-3 py-1 bg-blue-600 text-white rounded">Click me</button>"#;
454 let output = transpile_jsx_simple(input).unwrap();
455
456 assert!(output.contains("className"));
457 assert!(output.contains("px-3 py-1 bg-blue-600 text-white rounded"));
458 assert!(output.contains("Click me"));
459 }
460
461 #[test]
462 fn test_full_test_hook_transpiles() {
463 let input = include_str!("../tests/web/public/hooks/test-hook.jsx");
465 let output = transpile_jsx_simple(input);
466
467 assert!(output.is_ok(), "test-hook.jsx should transpile: {output:?}");
468 }
469
470 #[test]
471 fn test_reserved_keywords_in_jsx_text() {
472 let input = r#"<p>This paragraph has reserved keywords like interface await default import export but should still render correctly.</p>"#;
474 let output = transpile_jsx_simple(input).unwrap();
475
476 assert!(output.contains("__hook_jsx_runtime.jsx"));
478 assert!(output.contains("p"));
479
480 assert!(output.contains("interface"));
482 assert!(output.contains("await"));
483 assert!(output.contains("default"));
484 assert!(output.contains("import"));
485 assert!(output.contains("export"));
486 }
487
488 #[test]
489 fn test_transform_es6_modules() {
490 let input = r#"import { useState } from 'react';
491export default function MyComponent() {
492 return <div>Test</div>;
493}"#;
494 let output = transform_es6_modules(input);
495
496 assert!(output.contains("const { useState } = require('react')"));
498
499 assert!(output.contains("module.exports.default = function MyComponent()"));
501 }
502
503 #[test]
504 fn test_transform_es6_named_exports() {
505 let input = "export { foo, bar as baz };";
506 let output = transform_es6_modules(input);
507
508 assert!(output.contains("module.exports.foo = foo"));
509 assert!(output.contains("module.exports.baz = bar"));
510 }
511
512 #[test]
513 fn test_transform_es6_side_effect_imports() {
514 let input = r#"import 'styles.css';"#;
515 let output = transform_es6_modules(input);
516
517 assert!(output.contains("require('styles.css')"));
518 }
519
520 #[test]
521 fn test_extract_imports_for_prefetch() {
522 let input = r#"
523import React from 'react';
524import { useState, useEffect } from 'react';
525import * as Utils from './utils';
526const lazyComp = import('./LazyComponent');
527import 'styles.css';
528"#;
529 let imports = extract_imports(input);
530
531 assert_eq!(imports.len(), 5);
533
534 assert_eq!(imports[0].module, "react");
536 assert!(imports[0].is_default);
537 assert!(!imports[0].is_lazy);
538
539 assert_eq!(imports[1].module, "react");
540 assert!(!imports[1].is_default);
541 assert_eq!(imports[1].imported.len(), 2);
542
543 assert_eq!(imports[2].module, "./utils");
544 assert!(imports[2].is_namespace);
545
546 assert_eq!(imports[3].module, "./LazyComponent");
547 assert!(imports[3].is_lazy);
548
549 assert_eq!(imports[4].module, "styles.css");
551 assert!(imports[4].imported.is_empty());
552 }
553
554 #[test]
555 fn test_extract_imports_with_aliases() {
556 let input = "import { useState as State, useEffect as Effect } from 'react';";
557 let imports = extract_imports(input);
558
559 assert_eq!(imports.len(), 1);
560 assert_eq!(imports[0].imported.len(), 2);
561 assert!(imports[0].imported.contains(&"State".to_string()));
562 assert!(imports[0].imported.contains(&"Effect".to_string()));
563 }
564
565 #[test]
566 fn test_extract_imports_scoped_packages() {
567 let input = "import { Logger } from '@myorg/logging';";
568 let imports = extract_imports(input);
569
570 assert_eq!(imports.len(), 1);
571 assert_eq!(imports[0].module, "@myorg/logging");
572 assert_eq!(imports[0].imported, vec!["Logger"]);
573 }
574
575 #[test]
576 fn test_combined_module_transformation_and_extraction() {
577 let source = r#"
578import React, { useState } from 'react';
579import './styles.css';
580
581export const useMyHook = () => {
582 const [state, setState] = useState(null);
583 return state;
584};
585
586export default function Component() {
587 return <div>Test</div>;
588}
589"#;
590
591 let imports = extract_imports(source);
593
594 assert!(imports.len() >= 2);
598
599 let react_imports: Vec<_> = imports.iter()
601 .filter(|i| i.module == "react")
602 .collect();
603 assert!(!react_imports.is_empty());
604
605 let styles_import = imports.iter()
607 .find(|i| i.module == "./styles.css");
608 assert!(styles_import.is_some());
609
610 let transformed = transform_es6_modules(source);
612 assert!(transformed.contains("require('react')") || transformed.contains("require(\"react\")"));
613 assert!(transformed.contains("require('./styles.css')") || transformed.contains("require(\"./styles.css\")"));
614 assert!(transformed.contains("module.exports.useMyHook"));
615 assert!(transformed.contains("module.exports.default"));
616 }
617
618 #[test]
619 fn test_lazy_import_extraction() {
620 let source = r#"
621const lazyForm = import('./forms/FormComponent');
622const lazyModal = import('./modals/Modal');
623"#;
624 let imports = extract_imports(source);
625
626 assert_eq!(imports.len(), 2);
627 assert!(imports[0].is_lazy);
628 assert!(imports[1].is_lazy);
629 assert_eq!(imports[0].module, "./forms/FormComponent");
630 assert_eq!(imports[1].module, "./modals/Modal");
631 }
632}