main
rs 137 lines 5.34 KB
Raw
1 use std::time::Instant;
2
3 use napi_derive::napi;
4 use react_compiler::entrypoint::PluginOptions;
5 use react_compiler::entrypoint::compile_program;
6 use react_compiler::timing::TimingEntry;
7 use react_compiler_ast::File;
8 use react_compiler_ast::scope::ScopeInfo;
9 use serde::Deserialize;
10
11 /// Deserialize JSON with no recursion limit (for deeply nested ASTs).
12 fn from_json_str<'de, T: Deserialize<'de>>(s: &'de str) -> serde_json::Result<T> {
13 let mut deserializer = serde_json::Deserializer::from_str(s);
14 deserializer.disable_recursion_limit();
15 T::deserialize(&mut deserializer)
16 }
17
18 /// Main entry point for the React Compiler.
19 ///
20 /// Receives a full program AST, scope information, and resolved options
21 /// as JSON strings. Returns a JSON string containing the CompileResult.
22 ///
23 /// This function is called by the JS shim (bridge.ts) via napi-rs.
24 /// Spawns a dedicated thread with 64MB stack to handle deeply nested ASTs
25 /// that would overflow the default Node.js thread stack.
26 #[napi]
27 pub fn compile(ast_json: String, scope_json: String, options_json: String) -> napi::Result<String> {
28 let handle = std::thread::Builder::new()
29 .stack_size(64 * 1024 * 1024) // 64MB stack
30 .spawn(move || compile_inner(ast_json, scope_json, options_json))
31 .map_err(|e| napi::Error::from_reason(format!("Failed to spawn compiler thread: {}", e)))?;
32
33 match handle.join() {
34 Ok(result) => result,
35 Err(panic_payload) => {
36 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
37 format!("Rust compiler panicked: {}", s)
38 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
39 format!("Rust compiler panicked: {}", s)
40 } else {
41 "Rust compiler panicked (unknown payload)".to_string()
42 };
43 Err(napi::Error::from_reason(msg))
44 }
45 }
46 }
47
48 fn compile_inner(
49 ast_json: String,
50 scope_json: String,
51 options_json: String,
52 ) -> napi::Result<String> {
53 // Check if profiling is enabled by peeking at the options JSON
54 let profiling = options_json.contains("\"__profiling\":true");
55
56 let deser_start = Instant::now();
57
58 let ast: File = from_json_str(&ast_json)
59 .map_err(|e| napi::Error::from_reason(format!("Failed to parse AST JSON: {}", e)))?;
60
61 let scope: ScopeInfo = from_json_str(&scope_json)
62 .map_err(|e| napi::Error::from_reason(format!("Failed to parse scope JSON: {}", e)))?;
63
64 let opts: PluginOptions = from_json_str(&options_json)
65 .map_err(|e| napi::Error::from_reason(format!("Failed to parse options JSON: {}", e)))?;
66
67 let deser_duration = deser_start.elapsed();
68
69 let compile_start = Instant::now();
70 let mut result = compile_program(ast, scope, opts);
71 let compile_duration = compile_start.elapsed();
72
73 // If profiling is enabled, prepend NAPI deserialization timing and append serialization timing
74 if profiling {
75 let napi_deser_entry = TimingEntry {
76 name: "napi_deserialize".to_string(),
77 duration_us: deser_duration.as_micros() as u64,
78 };
79
80 // Insert NAPI timing entries
81 match &mut result {
82 react_compiler::entrypoint::CompileResult::Success { timing, .. } => {
83 timing.insert(0, napi_deser_entry);
84 }
85 react_compiler::entrypoint::CompileResult::Error { timing, .. } => {
86 timing.insert(0, napi_deser_entry);
87 }
88 }
89
90 // Add compile_program duration (the total Rust compilation time including pass timing)
91 let compile_entry = TimingEntry {
92 name: "napi_compile_program".to_string(),
93 duration_us: compile_duration.as_micros() as u64,
94 };
95 match &mut result {
96 react_compiler::entrypoint::CompileResult::Success { timing, .. } => {
97 timing.push(compile_entry);
98 }
99 react_compiler::entrypoint::CompileResult::Error { timing, .. } => {
100 timing.push(compile_entry);
101 }
102 }
103 }
104
105 let ser_start = Instant::now();
106 let result_json = serde_json::to_string(&result)
107 .map_err(|e| napi::Error::from_reason(format!("Failed to serialize result: {}", e)))?;
108
109 if profiling {
110 // We need to inject the serialization timing into the already-serialized JSON.
111 // Since timing is a JSON array at the end of the result, we can append to it.
112 let ser_duration = ser_start.elapsed();
113 let ser_entry = format!(
114 r#"{{"name":"napi_serialize","duration_us":{}}}"#,
115 ser_duration.as_micros()
116 );
117
118 // Find the timing array in the JSON and append our entry
119 if let Some(pos) = result_json.rfind("\"timing\":[") {
120 // Find the closing ] of the timing array
121 let timing_start = pos + "\"timing\":[".len();
122 if let Some(close_bracket) = result_json[timing_start..].rfind(']') {
123 let abs_close = timing_start + close_bracket;
124 let mut patched = result_json[..abs_close].to_string();
125 if abs_close > timing_start {
126 // Array is non-empty, add comma
127 patched.push(',');
128 }
129 patched.push_str(&ser_entry);
130 patched.push_str(&result_json[abs_close..]);
131 return Ok(patched);
132 }
133 }
134 }
135
136 Ok(result_json)
137 }