| 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 | } |