diff --git a/engine/Cargo.lock b/engine/Cargo.lock index 432fbad736..8f3e6f328b 100644 --- a/engine/Cargo.lock +++ b/engine/Cargo.lock @@ -761,7 +761,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower", "tower-layer", @@ -784,7 +784,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -894,7 +894,7 @@ dependencies = [ "pathdiff 0.1.0", "pretty_assertions", "rand", - "reqwest", + "reqwest 0.12.12", "rstest", "scopeguard", "serde", @@ -1154,7 +1154,7 @@ dependencies = [ "pin-project-lite", "pretty_assertions", "regex", - "reqwest", + "reqwest 0.12.12", "reqwest-eventsource", "ring", "rstest", @@ -1222,7 +1222,7 @@ dependencies = [ "jsonish", "log", "once_cell", - "reqwest", + "reqwest 0.12.12", "serde", "serde-wasm-bindgen 0.6.5", "serde_json", @@ -3129,6 +3129,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -3895,11 +3908,14 @@ dependencies = [ "baml-lsp-types", "baml-runtime", "baml-types", + "base64 0.21.7", + "bytes", "colored 3.0.0", "crossbeam", "crossbeam-channel", "filetime", "futures-util", + "http 0.2.12", "ignore", "include_dir", "indexmap 2.9.0", @@ -3919,6 +3935,7 @@ dependencies = [ "path-absolutize", "pathdiff 0.2.3", "regex", + "reqwest 0.11.27", "rustc-hash 2.1.1", "schemars 0.8.22", "semver", @@ -5327,6 +5344,46 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "reqwest" version = "0.12.12" @@ -5344,7 +5401,7 @@ dependencies = [ "http-body-util", "hyper 1.6.0", "hyper-rustls 0.27.7", - "hyper-tls", + "hyper-tls 0.6.0", "hyper-util", "ipnet", "js-sys", @@ -5358,8 +5415,8 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", "tokio", "tokio-native-tls", "tokio-util", @@ -5385,7 +5442,7 @@ dependencies = [ "mime", "nom", "pin-project-lite", - "reqwest", + "reqwest 0.12.12", "thiserror 1.0.69", ] @@ -5640,7 +5697,7 @@ dependencies = [ "baml-types", "env_logger", "log", - "reqwest", + "reqwest 0.12.12", "serde_json", "tokio", "tracing", @@ -6251,6 +6308,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6271,6 +6334,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys 0.5.0", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -6279,7 +6353,17 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.1", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -6653,7 +6737,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", @@ -7739,6 +7823,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winsafe" version = "0.0.19" diff --git a/engine/baml-schema-wasm/src/runtime_wasm/mod.rs b/engine/baml-schema-wasm/src/runtime_wasm/mod.rs index baae960d9b..7214bb45a9 100644 --- a/engine/baml-schema-wasm/src/runtime_wasm/mod.rs +++ b/engine/baml-schema-wasm/src/runtime_wasm/mod.rs @@ -54,7 +54,8 @@ pub fn on_wasm_init() { if #[cfg(debug_assertions)] { const LOG_LEVEL: log::Level = log::Level::Debug; } else { - const LOG_LEVEL: log::Level = log::Level::Info; + // TODO uncomment this + const LOG_LEVEL: log::Level = log::Level::Debug; } }; // I dont think we need this line anymore -- seems to break logging if you add it. diff --git a/engine/language_server/Cargo.toml b/engine/language_server/Cargo.toml index 0c515fa010..57881929d9 100644 --- a/engine/language_server/Cargo.toml +++ b/engine/language_server/Cargo.toml @@ -76,6 +76,10 @@ warp = "0.3" futures-util = "0.3" include_dir = "0.7" mime_guess = "2.0.4" +base64 = "0.21" +reqwest = { version = "0.11", features = ["json"] } +http = "0.2" +bytes = "1.0" webbrowser = "0.8" diff --git a/engine/language_server/scripts/build.sh b/engine/language_server/scripts/build_dist.sh similarity index 83% rename from engine/language_server/scripts/build.sh rename to engine/language_server/scripts/build_dist.sh index 52d874b319..29ff5ba222 100755 --- a/engine/language_server/scripts/build.sh +++ b/engine/language_server/scripts/build_dist.sh @@ -1,8 +1,8 @@ #!/bin/bash -# This script is needed as the language_server embeds the web-panel dist -# directory in the playground_server.rs file. It builds all of the dependencies -# for it as well as the web-panel itself. +# This script installs dependencies for the frontend and builds it so that it +# appears under dist. This is needed as playground_server_helpers.rs embeds the dist +# directory. # Exit on error set -e diff --git a/engine/language_server/src/baml_project/mod.rs b/engine/language_server/src/baml_project/mod.rs index c1969ba069..f55f3b572a 100644 --- a/engine/language_server/src/baml_project/mod.rs +++ b/engine/language_server/src/baml_project/mod.rs @@ -63,7 +63,7 @@ pub struct BamlProject { impl Drop for BamlProject { fn drop(&mut self) { - tracing::info!("Dropping BamlProject"); + tracing::debug!("Dropping BamlProject"); } } @@ -83,7 +83,7 @@ impl std::fmt::Debug for BamlProject { impl BamlProject { pub fn new(root_dir: PathBuf) -> Self { - tracing::info!("Creating BamlProject for {}", root_dir.display()); + tracing::debug!("Creating BamlProject for {}", root_dir.display()); Self { root_dir_name: root_dir, files: HashMap::new(), @@ -172,7 +172,7 @@ impl BamlProject { let generated = match runtime.run_codegen(&all_files, no_version_check.unwrap_or(false)) { Ok(gen) => { let elapsed = start_time.elapsed(); - tracing::info!( + tracing::debug!( "Generated {:?} baml_clients in {:?}ms", gen.len(), elapsed.as_millis() @@ -181,7 +181,7 @@ impl BamlProject { } Err(e) => { let elapsed = start_time.elapsed(); - tracing::info!( + tracing::debug!( "Failed to run codegen in {:?}ms: {:?}", elapsed.as_millis(), e @@ -192,11 +192,11 @@ impl BamlProject { }; match generated.len() { - 1 => tracing::info!( + 1 => tracing::debug!( "Generated 1 baml_client: {}", generated[0].output_dir_full.display() ), - n => tracing::info!( + n => tracing::debug!( "Generated {n} baml_clients: {}", generated .iter() @@ -209,7 +209,7 @@ impl BamlProject { } pub fn set_unsaved_file(&mut self, document_key: &DocumentKey, content: Option) { - tracing::info!( + tracing::debug!( "Setting unsaved file: {}, {}", document_key.path().display(), content.clone().unwrap_or("None".to_string()) @@ -228,7 +228,7 @@ impl BamlProject { self.cached_runtime = None; } pub fn save_file(&mut self, document_key: &DocumentKey, content: &str) { - tracing::info!( + tracing::debug!( "Saving file: {}, {}", document_key.path().display(), content @@ -240,7 +240,7 @@ impl BamlProject { } pub fn update_file(&mut self, document_key: &DocumentKey, content: Option) { - tracing::info!( + tracing::debug!( "Updating file: {}, {}", document_key.path().display(), content.clone().unwrap_or("None".to_string()) @@ -303,7 +303,7 @@ impl BamlProject { ) -> Result { let mut all_files_for_hash = self.files.iter().collect::>(); - log::info!( + log::debug!( "Baml Project saved files: {:#?}, Unsaved files: {:#?}", all_files_for_hash.len(), self.unsaved_files.len() @@ -329,13 +329,13 @@ impl BamlProject { tracing::debug!("Runtime cache hit ({})", current_hash); return cached_result.clone(); } - tracing::info!( + tracing::debug!( "Runtime cache miss (hash mismatch: {} != {})", *cached_hash, current_hash ); } else { - tracing::info!("Runtime cache miss (no cache entry)"); + tracing::debug!("Runtime cache miss (no cache entry)"); } let files_for_runtime = self @@ -1007,7 +1007,7 @@ impl Project { } let elapsed = start_time.elapsed(); - tracing::info!("update_runtime took {:?}ms", elapsed.as_millis()); + tracing::debug!("update_runtime took {:?}ms", elapsed.as_millis()); Ok(()) } diff --git a/engine/language_server/src/playground/definitions.rs b/engine/language_server/src/playground/definitions.rs index d73088892d..a3f0c7e392 100644 --- a/engine/language_server/src/playground/definitions.rs +++ b/engine/language_server/src/playground/definitions.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; @@ -33,18 +33,29 @@ pub struct PlaygroundState { pub tx: broadcast::Sender, // Keep a reference to the receiver to prevent the channel from being closed _rx: broadcast::Receiver, + /// Buffer for events that occur before the first client connects. + pub event_buffer: VecDeque, + pub first_client_connected: bool, + /// Track the number of active connections + pub active_connections: usize, } impl Default for PlaygroundState { fn default() -> Self { - let (tx, rx) = broadcast::channel(100); - Self { tx, _rx: rx } + Self::new() } } impl PlaygroundState { pub fn new() -> Self { - Self::default() + let (tx, rx) = broadcast::channel(100); + Self { + tx, + _rx: rx, + event_buffer: VecDeque::new(), + first_client_connected: false, + active_connections: 0, + } } pub fn broadcast_update(&self, msg: String) -> anyhow::Result<()> { @@ -52,4 +63,35 @@ impl PlaygroundState { tracing::debug!("broadcast sent to {n} receivers"); Ok(()) } + + /// Push an event to the buffer if no clients are connected. + pub fn buffer_event(&mut self, event: String) { + if self.active_connections == 0 { + self.event_buffer.push_back(event); + } + } + + /// Drain the buffer, returning all buffered events. + pub fn drain_event_buffer(&mut self) -> Vec { + self.event_buffer.drain(..).collect() + } + + /// Mark that a client has connected. + pub fn mark_client_connected(&mut self) { + self.active_connections += 1; + if self.active_connections == 1 { + self.first_client_connected = true; + } + } + + /// Mark that a client has disconnected. + pub fn mark_client_disconnected(&mut self) { + if self.active_connections > 0 { + self.active_connections -= 1; + if self.active_connections == 0 { + self.first_client_connected = false; + tracing::info!("All clients disconnected, resetting connection state"); + } + } + } } diff --git a/engine/language_server/src/playground/mod.rs b/engine/language_server/src/playground/mod.rs index 4d75c768a9..cf34d88569 100644 --- a/engine/language_server/src/playground/mod.rs +++ b/engine/language_server/src/playground/mod.rs @@ -1,9 +1,11 @@ pub mod definitions; pub mod playground_server; pub mod playground_server_helpers; +pub mod playground_server_rpc; +pub mod proxy; pub use definitions::{FrontendMessage, PlaygroundState}; pub use playground_server::PlaygroundServer; pub use playground_server_helpers::{ - broadcast_function_change, broadcast_project_update, create_server_routes, + broadcast_function_change, broadcast_project_update, broadcast_test_run, create_server_routes, }; diff --git a/engine/language_server/src/playground/playground_server.rs b/engine/language_server/src/playground/playground_server.rs index 70fbfd3e9e..c549fe3661 100644 --- a/engine/language_server/src/playground/playground_server.rs +++ b/engine/language_server/src/playground/playground_server.rs @@ -7,6 +7,7 @@ use tokio::sync::RwLock; /// On the input port use crate::playground::definitions::PlaygroundState; use crate::{playground::playground_server_helpers::create_server_routes, session::Session}; +use crate::playground::proxy::ProxyServer; #[derive(Debug, Clone)] pub struct PlaygroundServer { @@ -22,7 +23,23 @@ impl PlaygroundServer { pub async fn run(self, port: u16) -> Result<()> { let routes = create_server_routes(self.state, self.session); - warp::serve(routes).try_bind(([127, 0, 0, 1], port)).await; + // Start the proxy server on a different port + let proxy_port = port + 1; // Use playground port + 1 for proxy + let proxy_server = ProxyServer::new(proxy_port); + + // Spawn the proxy server in a separate task + let proxy_handle = tokio::spawn(async move { + if let Err(e) = proxy_server.start().await { + tracing::error!("Proxy server failed: {}", e); + } + }); + + // Start the main playground server + tracing::info!("Starting main playground server on port {}", port); + warp::serve(routes).run(([127, 0, 0, 1], port)).await; + + // If we get here, the main server has stopped + tracing::info!("Main playground server stopped"); Ok(()) } diff --git a/engine/language_server/src/playground/playground_server_helpers.rs b/engine/language_server/src/playground/playground_server_helpers.rs index 5bf7bd71e2..afd2e0cc5a 100644 --- a/engine/language_server/src/playground/playground_server_helpers.rs +++ b/engine/language_server/src/playground/playground_server_helpers.rs @@ -6,14 +6,21 @@ use include_dir::{include_dir, Dir}; use mime_guess::from_path; use tokio::sync::RwLock; use warp::{http::Response, ws::Message, Filter}; +use serde_json::Value; +use base64::engine::general_purpose; +use base64::Engine as _; use crate::{ playground::definitions::{FrontendMessage, PlaygroundState}, session::Session, + playground::playground_server_rpc::handle_rpc_websocket, }; /// Embed at compile time everything in dist/ -// WARNING: this is a relative path, will easily break if file structure changes +/// NOTE: If this line is throwing an ERROR then the script in language_server/scripts/install.sh +/// needs to be ran. +/// WARNING: this is a relative path, will easily break if file structure changes +/// WARNING: works as a macro so any build script executes after this is evaluated static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../typescript/vscode-ext/packages/web-panel/dist"); @@ -60,9 +67,28 @@ pub async fn start_client_connection( state.tx.subscribe() }; + // Mark client as connected + { + let mut st = state.write().await; + st.mark_client_connected(); + } + // Send initial project state using the helper send_all_projects_to_client(&mut ws_tx, &session).await; + // --- SEND BUFFERED EVENTS (if any) --- + { + let mut st = state.write().await; + let buffered_events = st.drain_event_buffer(); + for event in buffered_events.clone() { + let _ = ws_tx.send(Message::text(event)).await; + // Add configurable delay between buffered events + tokio::time::sleep(tokio::time::Duration::from_millis(400)).await; + } + tracing::info!("Sent {} buffered events", buffered_events.len()); + } + // --- END BUFFERED EVENTS --- + // Handle incoming messages and broadcast updates tokio::spawn(async move { loop { @@ -73,11 +99,17 @@ pub async fn start_client_connection( Ok(msg) => { if msg.is_close() { tracing::info!("Client disconnected"); + // Mark client as disconnected + let mut st = state.write().await; + st.mark_client_disconnected(); break; } } Err(e) => { tracing::error!("WebSocket error: {}", e); + // Mark client as disconnected on error + let mut st = state.write().await; + st.mark_client_disconnected(); break; } } @@ -86,10 +118,18 @@ pub async fn start_client_connection( Ok(msg) = rx.recv() => { if let Err(e) = ws_tx.send(Message::text(msg)).await { tracing::error!("Failed to send broadcast message: {}", e); + // Mark client as disconnected on send error + let mut st = state.write().await; + st.mark_client_disconnected(); break; } } - else => break, + else => { + // Mark client as disconnected when loop ends + let mut st = state.write().await; + st.mark_client_disconnected(); + break; + } } } }); @@ -102,15 +142,32 @@ pub fn create_server_routes( session: Arc, ) -> impl Filter + Clone { // WebSocket handler with error handling + let ws_state = state.clone(); + let ws_session = session.clone(); let ws_route = warp::path("ws") .and(warp::ws()) .map(move |ws: warp::ws::Ws| { - let state = state.clone(); - let session = session.clone(); + let state = ws_state.clone(); + let session = ws_session.clone(); ws.on_upgrade(move |socket| async move { start_client_connection(socket, state, session).await; }) }); + + tracing::info!("Setting up RPC websocket..."); + // RPC WebSocket handler + let rpc_session = session.clone(); + let rpc_route = warp::path("rpc") + .and(warp::ws()) + .map(move |ws: warp::ws::Ws| { + let session = rpc_session.clone(); + ws.on_upgrade(move |socket| async move { + handle_rpc_websocket(socket, session).await; + }) + }); + + // Static file serving for user files (e.g., images, data) + let static_files = warp::path("static").and(warp::fs::dir(".")); // Static file serving needed to serve the frontend files let spa = @@ -135,7 +192,11 @@ pub fn create_server_routes( } }); - ws_route.or(spa).with(warp::log("playground-server")) + ws_route + .or(rpc_route) + .or(static_files) + .or(spa) + .with(warp::log("playground-server")) } // Helper function to broadcast project updates with better error handling @@ -150,7 +211,10 @@ pub async fn broadcast_project_update( }; let msg_str = serde_json::to_string(&add_project_msg)?; - if let Err(e) = state.read().await.broadcast_update(msg_str) { + let mut st = state.write().await; + if !st.first_client_connected { + st.buffer_event(msg_str); + } else if let Err(e) = st.broadcast_update(msg_str) { tracing::error!("Failed to broadcast project update: {}", e); } Ok(()) @@ -164,14 +228,38 @@ pub async fn broadcast_function_change( ) -> Result<()> { tracing::debug!("Broadcasting function change for: {}", function_name); + // broadcast to all connected clients let select_function_msg = FrontendMessage::select_function { root_path: root_path.to_string(), function_name, }; let msg_str = serde_json::to_string(&select_function_msg)?; - if let Err(e) = state.read().await.broadcast_update(msg_str) { + let mut st = state.write().await; + if !st.first_client_connected { + st.buffer_event(msg_str); + } else if let Err(e) = st.broadcast_update(msg_str) { tracing::error!("Failed to broadcast function change: {}", e); } Ok(()) } + +// Helper function to broadcast test runs +pub async fn broadcast_test_run( + state: &Arc>, + test_name: String, +) -> Result<()> { + tracing::debug!("Broadcasting test run for: {}", test_name); + + // broadcast to all connected clients + let run_test_msg = FrontendMessage::run_test { test_name }; + + let msg_str = serde_json::to_string(&run_test_msg)?; + let mut st = state.write().await; + if !st.first_client_connected { + st.buffer_event(msg_str); + } else if let Err(e) = st.broadcast_update(msg_str) { + tracing::error!("Failed to broadcast test run: {}", e); + } + Ok(()) +} diff --git a/engine/language_server/src/playground/playground_server_rpc.rs b/engine/language_server/src/playground/playground_server_rpc.rs new file mode 100644 index 0000000000..0e350c55e3 --- /dev/null +++ b/engine/language_server/src/playground/playground_server_rpc.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use serde_json::Value; +use warp::ws::{Message, WebSocket}; + +use crate::session::Session; +use crate::playground::definitions::PlaygroundState; + +use base64::engine::general_purpose; +use base64::Engine as _; +use futures_util::{StreamExt, SinkExt}; +use mime_guess::from_path; + +/// Handles all playground RPC commands over the WebSocket connection. +pub async fn handle_rpc_websocket(ws: WebSocket, session: Arc) { + let (mut ws_tx, mut ws_rx) = ws.split(); + while let Some(Ok(msg)) = ws_rx.next().await { + if msg.is_text() { + if let Ok(json) = serde_json::from_str::(msg.to_str().unwrap()) { + let rpc_id = json["rpcId"].clone(); + let rpc_method = json["rpcMethod"].as_str().unwrap_or(""); + let data = &json["data"]; + // tracing::info!("Handling RPC request!"); + // tracing::info!("RPC METHOD: {:?}", rpc_method); + match rpc_method { + "INITIALIZED" => { + let response = serde_json::json!({ + "rpcMethod": "INITIALIZED", + "rpcId": rpc_id, + "data": { "ok": true } + }); + let _ = ws_tx.send(Message::text(response.to_string())).await; + } + "GET_WEBVIEW_URI" => { + let path = data["path"].as_str().unwrap_or(""); + let port = session.baml_settings.playground_port.unwrap_or(3030); + + // Convert absolute path to relative path for /static/ URI + let rel_path = std::env::current_dir() + .ok() + .and_then(|cwd| std::path::Path::new(path).strip_prefix(&cwd).ok()) + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|| path.to_string()); + + let uri = format!("http://localhost:{}/static/{}", port, rel_path); + let mut response_data = serde_json::json!({ "uri": uri }); + + // For non-image files, include contents as base64 + let mime = from_path(path).first_or_octet_stream(); + if !mime.type_().as_str().eq("image") { + if let Ok(contents) = std::fs::read(path) { + let base64 = general_purpose::STANDARD.encode(&contents); + response_data["contents"] = serde_json::Value::String(base64); + } + } + + let response = serde_json::json!({ + "rpcMethod": "GET_WEBVIEW_URI", + "rpcId": rpc_id, + "data": response_data + }); + let _ = ws_tx.send(Message::text(response.to_string())).await; + } + "GET_PLAYGROUND_PORT" => { + let playground_port = session.baml_settings.playground_port.unwrap_or(3030); + let response = serde_json::json!({ + "rpcMethod": "GET_PLAYGROUND_PORT", + "rpcId": rpc_id, + "data": { "port": playground_port } + }); + let _ = ws_tx.send(Message::text(response.to_string())).await; + } + "SET_PROXY_SETTINGS" => { + let response = serde_json::json!({ + "rpcMethod": "SET_PROXY_SETTINGS", + "rpcId": rpc_id, + "data": { "ok": true } + }); + let _ = ws_tx.send(Message::text(response.to_string())).await; + } + "LOAD_AWS_CREDS" => { + let response = serde_json::json!({ + "rpcMethod": "LOAD_AWS_CREDS", + "rpcId": rpc_id, + "data": { "ok": true } + }); + let _ = ws_tx.send(Message::text(response.to_string())).await; + } + "LOAD_GCP_CREDS" => { + let response = serde_json::json!({ + "rpcMethod": "LOAD_GCP_CREDS", + "rpcId": rpc_id, + "data": { "ok": true } + }); + let _ = ws_tx.send(Message::text(response.to_string())).await; + } + _ => { + tracing::warn!("Unknown RPC method: {}", rpc_method); + } + } + } + } + } +} \ No newline at end of file diff --git a/engine/language_server/src/playground/proxy.rs b/engine/language_server/src/playground/proxy.rs new file mode 100644 index 0000000000..606c958a99 --- /dev/null +++ b/engine/language_server/src/playground/proxy.rs @@ -0,0 +1,241 @@ +use std::sync::Arc; +use anyhow::Result; +use warp::{Filter, Reply, Rejection}; +use serde_json::Value; + +// Custom response type for binary data +struct BinaryResponse { + body: Vec, + status: http::StatusCode, +} + +impl warp::Reply for BinaryResponse { + fn into_response(self) -> warp::http::Response { + warp::http::Response::builder() + .status(self.status) + .header("access-control-allow-origin", "*") + .body(warp::hyper::Body::from(self.body)) + .unwrap() + } +} + +// Custom error type for proxy +#[derive(Debug)] +struct ProxyError; + +impl warp::reject::Reject for ProxyError {} + +// API keys for model providers - these should be injected into requests +const API_KEY_INJECTION_ALLOWED: &[(&str, &str, &str, &str)] = &[ + ("https://api.openai.com", "Authorization", "OPENAI_API_KEY", "baml-openai-api-key"), + ("https://api.anthropic.com", "x-api-key", "ANTHROPIC_API_KEY", "baml-anthropic-api-key"), + ("https://generativelanguage.googleapis.com", "x-goog-api-key", "GOOGLE_API_KEY", "baml-google-api-key"), + ("https://openrouter.ai", "Authorization", "OPENROUTER_API_KEY", "baml-openrouter-api-key"), + ("https://api.llmapi.com", "Authorization", "LLAMA_API_KEY", "baml-llama-api-key"), +]; + +// Temporary dummy API keys for testing (remove in production) +const DUMMY_API_KEYS: &[(&str, &str)] = &[ + ("OPENAI_API_KEY", "sk-dummy-openai-key-for-testing-only"), + ("ANTHROPIC_API_KEY", "sk-ant-dummy-anthropic-key-for-testing-only"), + ("GOOGLE_API_KEY", "dummy-google-api-key-for-testing-only"), + ("OPENROUTER_API_KEY", "sk-dummy-openrouter-key-for-testing-only"), + ("LLAMA_API_KEY", "sk-dummy-llama-key-for-testing-only"), +]; + +pub struct ProxyServer { + port: u16, +} + +impl ProxyServer { + pub fn new(port: u16) -> Self { + Self { port } + } + + pub async fn start(self) -> Result<()> { + let addr = ([127, 0, 0, 1], self.port); + + // Handle OPTIONS requests (preflight CORS requests) + let cors_route = warp::options() + .and(warp::path::tail()) + .map(|_| { + warp::http::Response::builder() + .status(http::StatusCode::OK) + .header("access-control-allow-origin", "*") + .header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS") + .header("access-control-allow-headers", "Content-Type, Authorization, x-api-key, baml-original-url, baml-openai-api-key, baml-anthropic-api-key, baml-google-api-key, baml-openrouter-api-key, baml-llama-api-key") + .header("access-control-max-age", "86400") + .body(warp::hyper::Body::empty()) + .unwrap() + }); + + // Proxy all requests - use a catch-all route that matches any path + let proxy_route = warp::any() + .and(warp::body::bytes()) + .and(warp::method()) + .and(warp::path::tail()) // Use tail() to capture any path + .and(warp::header::headers_cloned()) + .and_then(handle_proxy_request); + + // Combine CORS and proxy routes + let routes = cors_route.or(proxy_route); + + tracing::info!("Proxy server listening on port {}", self.port); + warp::serve(routes) + .run(addr) + .await; + + Ok(()) + } +} + +async fn handle_proxy_request( + body: bytes::Bytes, + method: http::Method, + path: warp::path::Tail, + mut headers: http::HeaderMap, +) -> Result { + let path_str = path.as_str(); + let original_url = match headers.get("baml-original-url") { + Some(url) => match url.to_str() { + Ok(s) => s.to_string(), + Err(_) => { + return Ok(BinaryResponse { body: Vec::new(), status: http::StatusCode::BAD_REQUEST }); + } + }, + None => { + return Ok(BinaryResponse { body: Vec::new(), status: http::StatusCode::BAD_REQUEST }); + } + }; + + headers.remove("baml-original-url"); + headers.remove("origin"); + headers.remove("authorization"); + headers.remove("host"); + + let clean_original_url = if original_url.ends_with('/') { + &original_url[..original_url.len() - 1] + } else { + &original_url + }; + + let url = match url::Url::parse(clean_original_url) { + Ok(url) => url, + Err(_) => { + return Ok(BinaryResponse { body: Vec::new(), status: http::StatusCode::BAD_REQUEST }); + } + }; + + if path_str.matches('.').count() == 1 && method == http::Method::GET { + return Ok(BinaryResponse { body: Vec::new(), status: http::StatusCode::OK }); + } + + let mut target_url = url.clone(); + + let base_path = if url.path().ends_with('/') { + url.path().trim_end_matches('/').to_string() + } else { + url.path().to_string() + }; + + let final_path = if base_path.is_empty() { + if let Some(stripped) = path_str.strip_suffix('/') { + stripped.to_string() + } else { + path_str.to_string() + } + } else if !path_str.starts_with(&base_path) { + if path_str.starts_with('/') { + format!("{}{}", base_path, path_str) + } else { + format!("{}/{}", base_path, path_str) + } + } else { + path_str.to_string() + }; + + let clean_final_path = if final_path.ends_with('/') { + &final_path[..final_path.len() - 1] + } else { + &final_path + }; + + target_url.set_path(clean_final_path); + + // Create the request to the target + let mut request_builder = http::Request::builder() + .method(method.clone()) + .uri(target_url.to_string()); + + // Add headers + for (name, value) in headers.iter() { + request_builder = request_builder.header(name.as_str(), value); + } + + // Inject API keys for allowed origins (same as VSCode extension) + let origin_str = match url.origin() { + url::Origin::Tuple(scheme, host, port) => { + match (scheme.as_str(), port) { + ("http", 80) | ("https", 443) => format!("{}://{}", scheme, host), + _ => format!("{}://{}:{}", scheme, host, port) + } + } + url::Origin::Opaque(_) => { + return Ok(BinaryResponse { body: Vec::new(), status: http::StatusCode::BAD_REQUEST }); + } + }; + + // API key injection logic + for (allowed_origin, header_name, env_var, baml_header) in API_KEY_INJECTION_ALLOWED { + if origin_str == *allowed_origin { + let api_key = std::env::var(env_var) + .ok() + .or_else(|| headers.get(*baml_header).and_then(|v| v.to_str().ok()).map(|s| s.to_string())) + .or_else(|| DUMMY_API_KEYS.iter().find(|(key, _)| *key == *env_var).map(|(_, v)| v.to_string())); + if let Some(api_key) = api_key { + let header_value = if *header_name == "Authorization" { + format!("Bearer {}", api_key) + } else { + api_key + }; + request_builder = request_builder.header(*header_name, header_value); + } + } + } + + let request = match request_builder.body(body.to_vec()) { + Ok(req) => req, + Err(_) => { + return Ok(BinaryResponse { body: Vec::new(), status: http::StatusCode::INTERNAL_SERVER_ERROR }); + } + }; + + let client = reqwest::Client::new(); + let response = match client.execute(request.try_into().map_err(|_| warp::reject::custom(ProxyError))?).await { + Ok(resp) => resp, + Err(e) => { + return Ok(BinaryResponse { body: Vec::new(), status: http::StatusCode::BAD_GATEWAY }); + } + }; + + let status = response.status(); + let body_bytes = match response.bytes().await { + Ok(bytes) => bytes, + Err(e) => { + return Ok(BinaryResponse { body: Vec::new(), status: http::StatusCode::INTERNAL_SERVER_ERROR }); + } + }; + + tracing::info!( + "[PROXY] {} {} → {:?} | headers: {:?} | req_body_len: {} | resp_status: {} | resp_body_len: {}", + method, + path_str, + url.origin(), + headers, + body.len(), + status, + body_bytes.len() + ); + + Ok(BinaryResponse { body: body_bytes.to_vec(), status }) +} \ No newline at end of file diff --git a/engine/language_server/src/server.rs b/engine/language_server/src/server.rs index eab160f053..05f03f7927 100644 --- a/engine/language_server/src/server.rs +++ b/engine/language_server/src/server.rs @@ -20,6 +20,7 @@ use lsp_types::{ WorkspaceClientCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }; use schedule::Task; +use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use self::{ @@ -53,6 +54,17 @@ pub(crate) struct Server { pub session: Session, } +#[derive(Serialize, Deserialize)] +struct PortNotificationParams { + port: u16, +} + +impl PortNotificationParams { + fn new(port: u16) -> Self { + PortNotificationParams { port } + } +} + impl Server { pub fn new(worker_threads: NonZeroUsize) -> anyhow::Result { let connection = ConnectionInitializer::stdio(); @@ -359,7 +371,10 @@ impl Server { }), code_action_provider: Some(lsp_types::CodeActionProviderCapability::Simple(true)), execute_command_provider: Some(lsp_types::ExecuteCommandOptions { - commands: vec!["openPlayground".to_string()], + commands: vec![ + "openPlayground".to_string(), + "baml.changeFunction".to_string(), + ], work_done_progress_options: Default::default(), }), definition_provider: Some(lsp_types::OneOf::Left(true)), @@ -396,6 +411,8 @@ impl Server { let mut playground_port = self.session.baml_settings.playground_port.unwrap_or(3030); let session_arc = Arc::new(self.session.clone()); let playground_server = PlaygroundServer::new(playground_state.clone(), session_arc); + let sender = self.connection.make_sender(); + rt.spawn(async move { loop { // Check if port is available before attempting to bind @@ -409,12 +426,17 @@ impl Server { "Hosted playground at http://localhost:{}...", playground_port ); - // Open the default browser - // if let Err(e) = - // webbrowser::open(&format!("http://localhost:{}", playground_port)) - // { - // tracing::warn!("Failed to open browser: {}", e); - // } + + // Send LSP notification about the port + let params = PortNotificationParams::new(playground_port); + let notification = lsp_server::Notification::new( + "baml/port".to_string(), + serde_json::to_value(params).unwrap(), + ); + if let Err(e) = sender.send(Message::Notification(notification)) { + tracing::error!("Failed to send port notification: {}", e); + } + server.run(playground_port).await.unwrap(); break; } else { diff --git a/engine/language_server/src/server/api.rs b/engine/language_server/src/server/api.rs index ad595cc8f2..f73fb5dc13 100644 --- a/engine/language_server/src/server/api.rs +++ b/engine/language_server/src/server/api.rs @@ -67,15 +67,16 @@ pub(super) fn request<'a>(req: lsp_server::Request) -> Task<'a> { } request::Completion::METHOD => local_request_task::(req), request::CodeLens::METHOD => local_request_task::(req), + request::CodeLensResolve::METHOD => local_request_task::(req), request::GotoDefinition::METHOD => local_request_task::(req), request::Rename::METHOD => local_request_task::(req), request::DocumentDiagnosticRequestHandler::METHOD => { - tracing::info!("diagnostic notif"); + // tracing::info!("diagnostic notif"); local_request_task::(req) // note background request task here sometimes results in inconsistent baml project state... } "getBAMLFunctions" => { - tracing::info!("getBAMLFunctions"); + // tracing::info!("getBAMLFunctions"); return Task::local(move |session, _notifier, requester, responder| { let result: anyhow::Result<(serde_json::Value,)> = { let mut all_functions = Vec::new(); @@ -120,7 +121,7 @@ pub(super) fn request<'a>(req: lsp_server::Request) -> Task<'a> { }); } "requestDiagnostics" => { - tracing::info!("---- requestDiagnostics"); + // tracing::info!("---- requestDiagnostics"); return Task::local(move |session, notifier, _requester, responder| { let result: anyhow::Result<()> = (|| { // tracing::info!("requestDiagnostics: {:?}", req.params); @@ -140,7 +141,7 @@ pub(super) fn request<'a>(req: lsp_server::Request) -> Task<'a> { // TODO: I think we need to send ALL diagnostics for the project. Not sure how this report is different vs sending a signle diagnostic param message let diagnostics = file_diagnostics(project.clone(), &url); - tracing::info!("---- diagnostics Returned: "); + // tracing::info!("---- diagnostics Returned: "); let report = Ok(DocumentDiagnosticReportResult::Report( DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { related_documents: None, @@ -232,7 +233,7 @@ pub(super) fn notification<'a>(notif: lsp_server::Notification) -> Vec> } // --- DidSaveTextDocument now uses the simple local task helper --- notification::DidSaveTextDocument::METHOD => { - tracing::info!("Did save text document---------"); + // tracing::info!("Did save text document---------"); handle_notification_result_error::( // Do not use background notifs yet, as baml_client may not have an updated view of the project files // See the did_save_text_document.rs file for more details @@ -275,10 +276,10 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>( let Some(_snapshot) = session.take_snapshot(url) else { return Box::new(|_, _| {}); }; - info!( - "session.projects.len(): {:?}", - session.baml_src_projects.lock().unwrap().len() - ); + // info!( + // "session.projects.len(): {:?}", + // session.baml_src_projects.lock().unwrap().len() + // ); let _db = session.get_or_create_project(&path).clone(); if _db.is_none() { tracing::error!("Could not find project for path"); diff --git a/engine/language_server/src/server/api/requests.rs b/engine/language_server/src/server/api/requests.rs index ab4a01c295..57c4db7334 100644 --- a/engine/language_server/src/server/api/requests.rs +++ b/engine/language_server/src/server/api/requests.rs @@ -9,7 +9,7 @@ mod hover; mod rename; pub use code_action::CodeActionHandler; -pub use code_lens::CodeLens; +pub use code_lens::{CodeLens, CodeLensResolve}; pub(super) use completion::Completion; pub(super) use diagnostic::DocumentDiagnosticRequestHandler; pub use execute_command::ExecuteCommand; diff --git a/engine/language_server/src/server/api/requests/code_lens.rs b/engine/language_server/src/server/api/requests/code_lens.rs index a162bb4d2a..24604c1455 100644 --- a/engine/language_server/src/server/api/requests/code_lens.rs +++ b/engine/language_server/src/server/api/requests/code_lens.rs @@ -127,7 +127,6 @@ impl SyncRequestHandler for CodeLens { // &state, // &root_path, // function_name, - // HashMap::new(), // We don't need the files map anymore // ) // .await; // }); @@ -172,3 +171,20 @@ impl SyncRequestHandler for CodeLens { Ok(Some(function_lenses)) } } + +pub struct CodeLensResolve; + +impl RequestHandler for CodeLensResolve { + type RequestType = request::CodeLensResolve; +} + +impl SyncRequestHandler for CodeLensResolve { + fn run( + session: &mut Session, + notifier: Notifier, + _requester: &mut Requester, + params: lsp_types::CodeLens, + ) -> Result { + Ok(params) + } +} diff --git a/engine/language_server/src/server/api/requests/execute_command.rs b/engine/language_server/src/server/api/requests/execute_command.rs index 4b797cd6f3..434b57adc3 100644 --- a/engine/language_server/src/server/api/requests/execute_command.rs +++ b/engine/language_server/src/server/api/requests/execute_command.rs @@ -61,8 +61,6 @@ impl SyncRequestHandler for ExecuteCommand { let state = state.clone(); if let Some(runtime) = &session.playground_runtime { runtime.spawn(async move { - // Wait a bit for the server to be ready - sleep(Duration::from_millis(500)).await; let _ = crate::playground::broadcast_function_change( &state, &function_name.to_string(), @@ -73,6 +71,80 @@ impl SyncRequestHandler for ExecuteCommand { } } } + } else if params.command == "baml.changeFunction" { + // Logic for getting the function can be improved + if let Some(state) = &session.playground_state { + if let Some(args) = params.arguments.first().and_then(|arg| arg.as_object()) { + if let (Some(function_name), Some(project_id)) = ( + args.get("functionName").and_then(|v| v.as_str()), + args.get("projectId").and_then(|v| v.as_str()), + ) { + tracing::info!( + "Broadcasting test run for function: {}", + function_name + ); + + // Set the selected function + let state_clone = state.clone(); + let func_name = function_name.to_string(); + let project_path = project_id.to_string(); + if let Some(runtime) = &session.playground_runtime { + runtime.spawn(async move { + let _ = crate::playground::broadcast_function_change( + &state_clone, + &project_path, + func_name, + ) + .await; + }); + } + } + } + } + } else if params.command == "baml.runTest" { + // Logic for running a test + if let Some(state) = &session.playground_state { + if let Some(args) = params.arguments.first().and_then(|arg| arg.as_object()) { + if let (Some(test_case_name), Some(function_name), Some(project_id)) = ( + args.get("testCaseName").and_then(|v| v.as_str()), + args.get("functionName").and_then(|v| v.as_str()), + args.get("projectId").and_then(|v| v.as_str()), + ) { + tracing::info!( + "Broadcasting test run for: {} in function: {}", + test_case_name, + function_name + ); + + // First, set the selected function + // TODO: test run should handle this in the future + let state_clone = state.clone(); + let func_name = function_name.to_string(); + let project_path = project_id.to_string(); + if let Some(runtime) = &session.playground_runtime { + runtime.spawn(async move { + let _ = crate::playground::broadcast_function_change( + &state_clone, + &project_path, + func_name, + ) + .await; + }); + } + + // Then, broadcast the test run + let state_clone = state.clone(); + let test_name = test_case_name.to_string(); + if let Some(runtime) = &session.playground_runtime { + runtime.spawn(async move { + let _ = + crate::playground::broadcast_test_run(&state_clone, test_name) + .await; + }); + } + } + } + } } else { return Err(crate::server::api::Error { code: ErrorCode::MethodNotFound, diff --git a/engine/zed/src/lib.rs b/engine/zed/src/lib.rs index 278b54df6f..06b7f9f10f 100644 --- a/engine/zed/src/lib.rs +++ b/engine/zed/src/lib.rs @@ -41,11 +41,13 @@ impl BamlExtension { } if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(BamlBinary { - path: path.clone(), - args: binary_args, - }); + if let Ok(stat) = fs::metadata(path) { + if stat.is_file() { + return Ok(BamlBinary { + path: path.clone(), + args: binary_args, + }); + } } } @@ -106,7 +108,7 @@ impl BamlExtension { let version_dir = format!("baml-cli-{}", release.version); let binary_path = format!("{version_dir}/baml-cli"); - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/jetbrains/README.md b/jetbrains/README.md index 622651b1eb..b8610d9ca7 100644 --- a/jetbrains/README.md +++ b/jetbrains/README.md @@ -12,7 +12,7 @@ > Click the Use this template button and clone it in IntelliJ IDEA. -**IntelliJ Platform Plugin Template** is a repository that provides a pure template to make it easier to create a new plugin project (check the [Creating a repository from a template][gh:template] article). +**baml placeholder** is a repository that provides a pure template to make it easier to create a new plugin project (check the [Creating a repository from a template][gh:template] article). The main goal of this template is to speed up the setup phase of plugin development for both new and experienced developers by preconfiguring the project scaffold and CI, linking to the proper documentation pages, and keeping everything organized. diff --git a/jetbrains/build.gradle.kts b/jetbrains/build.gradle.kts index 6a8994acfe..e1d690fbf6 100644 --- a/jetbrains/build.gradle.kts +++ b/jetbrains/build.gradle.kts @@ -5,6 +5,7 @@ import org.jetbrains.intellij.platform.gradle.TestFrameworkType plugins { id("java") // Java support alias(libs.plugins.kotlin) // Kotlin support + alias(libs.plugins.kotlinSerialization) alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin alias(libs.plugins.changelog) // Gradle Changelog Plugin alias(libs.plugins.qodana) // Gradle Qodana Plugin @@ -46,6 +47,8 @@ dependencies { testFramework(TestFrameworkType.Platform) } + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") } // Configure IntelliJ Platform Gradle Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-extension.html @@ -55,17 +58,18 @@ intellijPlatform { version = providers.gradleProperty("pluginVersion") // Extract the section from README.md and provide for the plugin's manifest - description = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { - val start = "" - val end = "" - - with(it.lines()) { - if (!containsAll(listOf(start, end))) { - throw GradleException("Plugin description section not found in README.md:\n$start ... $end") - } - subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) - } - } + description = providers.fileContents(layout.projectDirectory.file("../README.md")).asText.map(::markdownToHTML) +// description = providers.fileContents(layout.projectDirectory.file("../README.md")).asText.map { +// val start = "" +// val end = "" +// +// with(it.lines()) { +// if (!containsAll(listOf(start, end))) { +// throw GradleException("Plugin description section not found in README.md:\n$start ... $end") +// } +// subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) +// } +// } val changelog = project.changelog // local variable for configuration cache compatibility // Get the latest available change notes from the changelog file diff --git a/jetbrains/gradle.properties b/jetbrains/gradle.properties index 7efa004426..63e4026349 100644 --- a/jetbrains/gradle.properties +++ b/jetbrains/gradle.properties @@ -5,7 +5,7 @@ pluginName = BoundaryML pluginId = baml pluginRepositoryUrl = https://github.com/boundaryml/baml # SemVer format -> https://semver.org -pluginVersion = 0.89.1-beta +pluginVersion = 0.88.4-beta # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # To depend on the TextMate bundle API, the plugin must start build from 241 @@ -22,7 +22,9 @@ platformVersion = 2024.2.5 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP -platformPlugins = +#platformPlugins = com.redhat.devtools.lsp4ij:0.13.0 +# platformPlugins = com.redhat.devtools.lsp4ij:0.13.1-20250530-194419@nightly +platformPlugins = com.redhat.devtools.lsp4ij:0.14.0 # Example: platformBundledPlugins = com.intellij.java platformBundledPlugins = org.jetbrains.plugins.textmate @@ -37,3 +39,6 @@ org.gradle.configuration-cache = true # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html org.gradle.caching = true + +# Kotlin daemon memory configuration to prevent OutOfMemoryError +kotlin.daemon.jvmargs=-Xmx2g diff --git a/jetbrains/gradle/libs.versions.toml b/jetbrains/gradle/libs.versions.toml index bead92d5b6..478264e43a 100644 --- a/jetbrains/gradle/libs.versions.toml +++ b/jetbrains/gradle/libs.versions.toml @@ -15,8 +15,9 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "opentest4j" } [plugins] -changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } -intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } -qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } +intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } \ No newline at end of file diff --git a/jetbrains/src/main/java/com/boundaryml/jetbrains_ext/ExampleJavaClass.java b/jetbrains/src/main/java/com/boundaryml/jetbrains_ext/ExampleJavaClass.java new file mode 100644 index 0000000000..59e417d038 --- /dev/null +++ b/jetbrains/src/main/java/com/boundaryml/jetbrains_ext/ExampleJavaClass.java @@ -0,0 +1,8 @@ +package com.boundaryml.jetbrains_ext; + +public class ExampleJavaClass { + + public ExampleJavaClass() { + System.out.println("We should use Kotlin by default, but if we need to copy-paste Java code it can go here."); + } +} diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/MyBundle.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlBundle.kt similarity index 82% rename from jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/MyBundle.kt rename to jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlBundle.kt index 220df7b334..4f24452445 100644 --- a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/MyBundle.kt +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlBundle.kt @@ -5,9 +5,9 @@ import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.PropertyKey @NonNls -private const val BUNDLE = "messages.MyBundle" +private const val BUNDLE: String = "messages.BamlBundle" -object MyBundle : DynamicBundle(BUNDLE) { +object BamlBundle : DynamicBundle(BUNDLE) { @JvmStatic fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlCustomServerAPI.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlCustomServerAPI.kt new file mode 100644 index 0000000000..7ccb36090d --- /dev/null +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlCustomServerAPI.kt @@ -0,0 +1,12 @@ +import com.intellij.openapi.application.Application +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.LanguageServer +import java.util.concurrent.CompletableFuture; + +data class PortParams(val port: Int) + +interface BamlCustomServerAPI : LanguageClient { + @JsonNotification("baml/port") + fun onPort(params: PortParams) +} \ No newline at end of file diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlGetPortService.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlGetPortService.kt new file mode 100644 index 0000000000..a63267149c --- /dev/null +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlGetPortService.kt @@ -0,0 +1,30 @@ +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import com.intellij.util.messages.Topic + + +@Service(Service.Level.PROJECT) +class BamlGetPortService(private val project: Project) { + + companion object { + val TOPIC = Topic.create( + "BAML-port", + Listener::class.java, + Topic.BroadcastDirection.NONE + ) + } + + @Volatile + var port: Int? = null + private set + + fun setPort(newPort: Int) { + port = newPort + project.messageBus + .syncPublisher(TOPIC) + .onPort(newPort) + } + + fun interface Listener { fun onPort(port: Int) } +} + diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlIcons.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlIcons.kt index d4fab6eed0..4ac41dc50b 100644 --- a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlIcons.kt +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlIcons.kt @@ -4,5 +4,6 @@ import com.intellij.openapi.util.IconLoader import javax.swing.Icon object BamlIcons { - val FILETYPE: Icon = IconLoader.getIcon("/icons/baml-lamb-purple.svg", BamlIcons::class.java) + @JvmField + public val FILETYPE: Icon = IconLoader.getIcon("/icons/baml-lamb-purple.svg", BamlIcons::class.java) } \ No newline at end of file diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguage.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguage.kt index 904b8f2d75..e3ede605bf 100644 --- a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguage.kt +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguage.kt @@ -1,5 +1,5 @@ package com.boundaryml.jetbrains_ext - +import com.redhat.devtools.lsp4ij.client.LanguageClientImpl import com.intellij.lang.Language object BamlLanguage : Language("baml") { diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageClient.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageClient.kt new file mode 100644 index 0000000000..6df4977570 --- /dev/null +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageClient.kt @@ -0,0 +1,19 @@ +package com.boundaryml.jetbrains_ext + +import BamlCustomServerAPI +import PortParams +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.redhat.devtools.lsp4ij.client.LanguageClientImpl + +class BamlLanguageClient(project: Project) : + LanguageClientImpl(project), BamlCustomServerAPI { + + private val log = Logger.getInstance(BamlLanguageClient::class.java) + + override fun onPort(params: PortParams) { + Logger.getInstance(javaClass).warn("Port params: ${params.port}") + project.getService(BamlGetPortService::class.java) + .setPort(params.port) + } +} diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageServer.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageServer.kt new file mode 100644 index 0000000000..eddd8b5f9e --- /dev/null +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageServer.kt @@ -0,0 +1,30 @@ +package com.boundaryml.jetbrains_ext + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.openapi.project.Project +import com.redhat.devtools.lsp4ij.server.OSProcessStreamConnectionProvider +import java.nio.file.Files +import java.nio.file.Path + +class BamlLanguageServer(private val project: Project) : OSProcessStreamConnectionProvider() { + + init { + // val commandLine = GeneralCommandLine(Path.of(System.getProperty("user.home"), ".baml/jetbrains", "baml-cli-0.89.0-aarch64-apple-darwin", "baml-cli").toString(), "lsp") + // UNCOMMENT FOR DEBUGGING LOCALLY +// val commandLine = GeneralCommandLine( +// Path.of(System.getProperty("user.home"), +// "/Documents/baml/engine/target/debug", "baml-cli").toString(), "lsp") +// super.setCommandLine(commandLine) + + val cacheDir = Path.of(System.getProperty("user.home"), ".baml/jetbrains") + val version = Files.readString(cacheDir.resolve("baml-cli-installed.txt")).trim() + + val (arch, platform, _) = BamlLanguageServerInstaller.getPlatformTriple() + val exe = if (platform == "pc-windows-msvc") "baml-cli.exe" else "baml-cli" + val cli = cacheDir.resolve("baml-cli-$version-$arch-$platform").resolve(exe) + + super.setCommandLine(GeneralCommandLine(cli.toString(), "lsp")) + + } + +} diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageServerFactory.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageServerFactory.kt new file mode 100644 index 0000000000..b203206be7 --- /dev/null +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageServerFactory.kt @@ -0,0 +1,27 @@ +package com.boundaryml.jetbrains_ext + +import com.intellij.openapi.project.Project +import com.redhat.devtools.lsp4ij.LanguageServerFactory +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures +import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider + +class BamlLanguageServerFactory : LanguageServerFactory { + + override fun createConnectionProvider(project: Project): StreamConnectionProvider { + return BamlLanguageServer(project) + } + + override fun createClientFeatures(): LSPClientFeatures { + val features = LSPClientFeatures() + features.setServerInstaller(BamlLanguageServerInstaller()) // customize language server installer + return features + } + + override fun createLanguageClient(project: Project) = + BamlLanguageClient(project) // our custom client + + // If you need to expose a custom server API +// override fun getServerInterface(): Class { +// return BamlCustomServerAPI.kt::class.java +// } +} diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageServerInstaller.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageServerInstaller.kt new file mode 100644 index 0000000000..0967763681 --- /dev/null +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlLanguageServerInstaller.kt @@ -0,0 +1,227 @@ +package com.boundaryml.jetbrains_ext + +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.util.io.HttpRequests +import com.redhat.devtools.lsp4ij.installation.LanguageServerInstallerBase +import kotlinx.serialization.SerialName +import org.apache.commons.compress.archivers.tar.TarArchiveEntry +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream +import java.nio.file.* +import java.nio.file.attribute.PosixFilePermission +import java.security.MessageDigest +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import java.util.zip.ZipInputStream + +class BamlLanguageServerInstaller : LanguageServerInstallerBase() { + + private val REPO = "BoundaryML/baml" + private val GH_API_LATEST = "https://api.github.com/repos/$REPO/releases/latest" + private val GH_RELEASES_BASE = "https://github.com/$REPO/releases/download" + + private val bamlCacheDir: Path = Path.of(System.getProperty("user.home"), ".baml/jetbrains") + private val breadcrumbFile: Path = bamlCacheDir.resolve("baml-cli-installed.txt") + + companion object { + /** arch, platform, extension (zip|tar.gz) */ + @JvmStatic + fun getPlatformTriple(): Triple { + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + + val releaseArch = when { + arch.contains("aarch64") || arch.contains("arm64") -> "aarch64" + arch.contains("x86_64") || arch.contains("amd64") -> "x86_64" + else -> error("Unsupported arch: $arch") + } + val releasePlatform = when { + os.contains("mac") -> "apple-darwin" + os.contains("win") -> "pc-windows-msvc" + os.contains("linux") -> "unknown-linux-gnu" + else -> error("Unsupported OS: $os") + } + val ext = if (releasePlatform == "pc-windows-msvc") "zip" else "tar.gz" + return Triple(releaseArch, releasePlatform, ext) + } + } + + override fun checkServerInstalled(indicator: ProgressIndicator): Boolean { + super.progress("Checking if BAML CLI is installed...", indicator) + ProgressManager.checkCanceled() + return Files.exists(breadcrumbFile) + } + + override fun install(indicator: ProgressIndicator) { + super.progress("Installing BAML CLI...", indicator) + ProgressManager.checkCanceled() + + val latestVersion = fetchLatestReleaseVersion(indicator) + val (arch, platform, extension) = getPlatformTriple() + + val artifactName = "baml-cli-$latestVersion-$arch-$platform" + val destDir = bamlCacheDir.resolve(artifactName) + val targetExecutable = if (platform == "pc-windows-msvc") + destDir.resolve("baml-cli.exe") + else + destDir.resolve("baml-cli") + + if (Files.exists(targetExecutable)) { + super.progress("BAML CLI already installed.", indicator) + return + } + + val archivePath = downloadFile(artifactName, extension, latestVersion, indicator) + verifyChecksum(artifactName, extension, latestVersion, archivePath, indicator) + + super.progress("Extracting BAML CLI...", indicator) + extractArchive(archivePath, extension, destDir) + + Files.deleteIfExists(archivePath) + setExecutable(targetExecutable) + + Files.createDirectories(bamlCacheDir) + Files.writeString(breadcrumbFile, latestVersion) + + super.progress("Installation complete!", 1.0, indicator) + } + + @Serializable + data class GitHubRelease( + @SerialName("tag_name") + val tagName: String + ) + + private fun fetchLatestReleaseVersion(indicator: ProgressIndicator): String { + super.progress("Fetching latest version info...", indicator) + ProgressManager.checkCanceled() + + return try { + val jsonText = HttpRequests.request(GH_API_LATEST).readString() + val jsonParser = Json { + ignoreUnknownKeys = true + } + val release = jsonParser.decodeFromString(jsonText) + + release.tagName.removePrefix("v") + } catch (e: Exception) { + super.progress("GitHub fetch failed, falling back to local cache...", indicator) + // hardcoded fallback to 0.89 + // TODO: fallback to latest downloaded version + "0.89.0" + } + } + + private fun downloadFile(artifactName: String, extension: String, version: String, indicator: ProgressIndicator): Path { + val url = "$GH_RELEASES_BASE/$version/$artifactName.$extension" + val tempFile = Files.createTempFile("baml-cli", ".$extension") + + super.progress("Downloading $artifactName...", indicator) + ProgressManager.checkCanceled() + + HttpRequests.request(url).connect { request -> + request.saveToFile(tempFile.toFile(), indicator) + } + + return tempFile + } + + private fun verifyChecksum(artifactName: String, extension: String, version: String, archivePath: Path, indicator: ProgressIndicator) { + super.progress("Verifying checksum...", indicator) + ProgressManager.checkCanceled() + + val checksumUrl = "$GH_RELEASES_BASE/$version/$artifactName.$extension.sha256" + val checksumContent = HttpRequests.request(checksumUrl).readString().trim() + val expectedChecksum = checksumContent.split(Regex("\\s+"))[0] + val actualChecksum = calculateSha256(archivePath) + + if (!expectedChecksum.equals(actualChecksum, ignoreCase = true)) { + throw IllegalStateException("Checksum mismatch! Expected $expectedChecksum, got $actualChecksum") + } + } + + private fun calculateSha256(file: Path): String { + val digest = MessageDigest.getInstance("SHA-256") + Files.newInputStream(file).use { stream -> + val buffer = ByteArray(8192) + var read: Int + while (stream.read(buffer).also { read = it } != -1) { + digest.update(buffer, 0, read) + } + } + return digest.digest().joinToString("") { "%02x".format(it) } + } + + private fun extractArchive(archivePath: Path, extension: String, destDir: Path) { + Files.createDirectories(destDir) + + if (extension == "tar.gz") { + Files.newInputStream(archivePath).use { fileIn -> + GzipCompressorInputStream(fileIn).use { gzipIn -> + TarArchiveInputStream(gzipIn).use { tarIn -> + var entry: TarArchiveEntry? = tarIn.nextTarEntry + while (entry != null) { + val sanitizedName = sanitizePath(entry.name) + val outPath = destDir.resolve(sanitizedName) + + // Ensure the resolved path is within the destination directory + if (!outPath.startsWith(destDir)) { + throw SecurityException("Archive entry path would escape destination directory: ${entry.name}") + } + + if (entry.isDirectory) { + Files.createDirectories(outPath) + } else { + Files.createDirectories(outPath.parent) + Files.copy(tarIn, outPath, StandardCopyOption.REPLACE_EXISTING) + } + entry = tarIn.nextTarEntry + } + } + } + } + } else if (extension == "zip") { + ZipInputStream(Files.newInputStream(archivePath)).use { zipIn -> + var entry = zipIn.nextEntry + while (entry != null) { + val sanitizedName = sanitizePath(entry.name) + val outPath = destDir.resolve(sanitizedName) + + // Ensure the resolved path is within the destination directory + if (!outPath.startsWith(destDir)) { + throw SecurityException("Archive entry path would escape destination directory: ${entry.name}") + } + + if (entry.isDirectory) { + Files.createDirectories(outPath) + } else { + Files.createDirectories(outPath.parent) + Files.copy(zipIn, outPath, StandardCopyOption.REPLACE_EXISTING) + } + entry = zipIn.nextEntry + } + } + } else { + throw IllegalArgumentException("Unsupported archive extension: $extension") + } + } + + private fun sanitizePath(path: String): String { + // Normalize the path and remove any ".." components + return Path.of(path).normalize().toString().replace("..", "") + } + + private fun setExecutable(file: Path) { + if (!Files.exists(file)) return + try { + val perms = Files.getPosixFilePermissions(file) + val updatedPerms = perms + setOf( + PosixFilePermission.OWNER_EXECUTE, + ) + Files.setPosixFilePermissions(file, updatedPerms) + } catch (e: UnsupportedOperationException) { + // Windows doesn't support POSIX permissions — safely ignore + } + } +} diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlProjectService.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlProjectService.kt deleted file mode 100644 index ce06c6eff4..0000000000 --- a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlProjectService.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.boundaryml.jetbrains_ext - -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.project.Project - -// This service runs in the background -@Service(Service.Level.PROJECT) -class BamlProjectService(project: Project) { - - init { - thisLogger().info(MyBundle.message("projectService", project.name)) - thisLogger().info("BAML Jetbrains extension service has started") - } - - fun getRandomNumber() = (1..100).random() -} \ No newline at end of file diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlTextMateBundleProvider.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlTextMateBundleProvider.kt index 4668e9cd74..f480d03a8c 100644 --- a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlTextMateBundleProvider.kt +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlTextMateBundleProvider.kt @@ -38,7 +38,13 @@ class BamlTextMateBundleProvider : TextMateBundleProvider { // and (2) build system shenanigans in jetbrains-plugin-fss for packaging the tmbundle. override fun getBundles(): List { try { - val tmpDir: Path = Files.createTempDirectory(Path.of(PathManager.getTempPath()), "textmate-baml") + val tempPath = Path.of(PathManager.getTempPath()) + // Ensure the temp directory exists + if (!Files.exists(tempPath)) { + Files.createDirectories(tempPath) + } + + val tmpDir: Path = Files.createTempDirectory(tempPath, "textmate-baml") files.forEach { fileToCopy -> val resource: URL? = javaClass.classLoader.getResource("textmate/$fileToCopy") diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlToolWindowFactory.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlToolWindowFactory.kt index efb59abd38..bf2cd5edaa 100644 --- a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlToolWindowFactory.kt +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/BamlToolWindowFactory.kt @@ -2,14 +2,52 @@ package com.boundaryml.jetbrains_ext import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.ui.content.ContentFactory import com.intellij.ui.jcef.JBCefBrowser import java.awt.BorderLayout -import java.awt.Container import javax.swing.JPanel +private const val PLACEHOLDER_HTML = """ + + + + + Loading BAML Playground… + + + +
+

Starting BAML Playground…

+ + +""" + class BamlToolWindowFactory : ToolWindowFactory { init { @@ -17,9 +55,32 @@ class BamlToolWindowFactory : ToolWindowFactory { } override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val myToolWindow = BamlToolWindow(toolWindow) - val content = ContentFactory.getInstance().createContent(myToolWindow.getContent(), null, false) + val browser = JBCefBrowser().apply { + loadHTML(PLACEHOLDER_HTML.trimIndent()) + } + + // show browser in a tool window + val panel = JPanel(BorderLayout()).apply { add(browser.component, BorderLayout.CENTER) } + val content = ContentFactory.getInstance().createContent(panel, null, false) toolWindow.contentManager.addContent(content) + + val savedPort = project.getService(BamlGetPortService::class.java).port + if (savedPort != null) { + // LS was up before the tool-window opened + browser.loadURL("http://localhost:$savedPort/") + } else { + // LS not ready yet wait for a port message + val busConnection = project.messageBus.connect(toolWindow.disposable) + busConnection.subscribe( + BamlGetPortService.TOPIC, + BamlGetPortService.Listener { port -> + browser.loadURL("http://localhost:$port/") + busConnection.disconnect() // one-shot, avoid duplicates + } + ) + } + + Disposer.register(toolWindow.disposable, browser) } override fun shouldBeAvailable(project: Project) = true @@ -29,20 +90,10 @@ class BamlToolWindowFactory : ToolWindowFactory { private val browser = JBCefBrowser() init { - var htmlContent = """ - - - - - - Hello World - - -
TODO: render the BAML playground here and wire up the vscode provider bridge
- - - """.trimIndent() - browser.loadHTML(htmlContent) + browser.loadHTML( + PLACEHOLDER_HTML.trimIndent() + ) + } fun getContent(): JPanel { @@ -51,7 +102,6 @@ class BamlToolWindowFactory : ToolWindowFactory { } } - // This approach doesn't work. // We need to follow instructions here and implement resource loaders // https://plugins.jetbrains.com/docs/intellij/embedded-browser-jcef.html#loading-resources-from-plugin-distribution diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/OpenBamlPlaygroundAction.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/OpenBamlPlaygroundAction.kt new file mode 100644 index 0000000000..b48895c813 --- /dev/null +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/OpenBamlPlaygroundAction.kt @@ -0,0 +1,25 @@ +package com.boundaryml.jetbrains_ext + +import com.intellij.openapi.wm.ToolWindowManager +import com.redhat.devtools.lsp4ij.commands.LSPCommandAction +import org.eclipse.lsp4j.ExecuteCommandParams +import com.redhat.devtools.lsp4ij.commands.LSPCommand +import com.intellij.openapi.actionSystem.AnActionEvent + +class OpenBamlPlaygroundAction : LSPCommandAction() { + + override fun commandPerformed(command: LSPCommand, e: AnActionEvent) { + val project = e.project ?: return + val toolWindow = ToolWindowManager.getInstance(project) + .getToolWindow("BAML Playground") + + val args: List = command.arguments + + toolWindow?.show { + val ls = getLanguageServer(e)?.server ?: return@show + ls.workspaceService.executeCommand( + ExecuteCommandParams("baml.changeFunction", args) + ) + } + } +} \ No newline at end of file diff --git a/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/RunBamlTestAction.kt b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/RunBamlTestAction.kt new file mode 100644 index 0000000000..6054cd74da --- /dev/null +++ b/jetbrains/src/main/kotlin/com/boundaryml/jetbrains_ext/RunBamlTestAction.kt @@ -0,0 +1,25 @@ +package com.boundaryml.jetbrains_ext + +import com.intellij.openapi.wm.ToolWindowManager +import com.redhat.devtools.lsp4ij.commands.LSPCommandAction +import com.redhat.devtools.lsp4ij.commands.LSPCommand +import org.eclipse.lsp4j.ExecuteCommandParams +import com.intellij.openapi.actionSystem.AnActionEvent + +class RunBamlTestAction : LSPCommandAction() { + + override fun commandPerformed(command: LSPCommand, e: AnActionEvent) { + val project = e.project ?: return + val toolWindow = ToolWindowManager.getInstance(project) + .getToolWindow("BAML Playground") + + val args: List = command.arguments + + toolWindow?.show { + val ls = getLanguageServer(e)?.server ?: return@show + ls.workspaceService.executeCommand( + ExecuteCommandParams("baml.runTest", args) + ) + } + } +} \ No newline at end of file diff --git a/jetbrains/src/main/resources/META-INF/baml-lsp4ij.xml b/jetbrains/src/main/resources/META-INF/baml-lsp4ij.xml new file mode 100644 index 0000000000..e3db68b73f --- /dev/null +++ b/jetbrains/src/main/resources/META-INF/baml-lsp4ij.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jetbrains/src/main/resources/META-INF/plugin.xml b/jetbrains/src/main/resources/META-INF/plugin.xml index 4443a7c324..c1c6c5f882 100644 --- a/jetbrains/src/main/resources/META-INF/plugin.xml +++ b/jetbrains/src/main/resources/META-INF/plugin.xml @@ -6,8 +6,28 @@ com.intellij.modules.platform org.jetbrains.plugins.textmate + com.redhat.devtools.lsp4ij - messages.MyBundle + messages.BamlBundle + + + + + + + + + diff --git a/jetbrains/src/main/resources/META-INF/pluginIcon.svg b/jetbrains/src/main/resources/META-INF/pluginIcon.svg deleted file mode 100644 index fb1499c55d..0000000000 --- a/jetbrains/src/main/resources/META-INF/pluginIcon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/jetbrains/src/main/resources/META-INF/pluginIcon.svg b/jetbrains/src/main/resources/META-INF/pluginIcon.svg new file mode 120000 index 0000000000..303b688230 --- /dev/null +++ b/jetbrains/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1 @@ +../icons/baml-lamb-purple.svg \ No newline at end of file diff --git a/jetbrains/src/main/resources/messages/MyBundle.properties b/jetbrains/src/main/resources/messages/BamlBundle.properties similarity index 100% rename from jetbrains/src/main/resources/messages/MyBundle.properties rename to jetbrains/src/main/resources/messages/BamlBundle.properties diff --git a/jetbrains/src/test/kotlin/com/boundaryml/jetbrains_ext/BamlPluginTest.kt b/jetbrains/src/test/kotlin/com/boundaryml/jetbrains_ext/BamlPluginTest.kt index ec6bf5a7d8..711b4e87ee 100644 --- a/jetbrains/src/test/kotlin/com/boundaryml/jetbrains_ext/BamlPluginTest.kt +++ b/jetbrains/src/test/kotlin/com/boundaryml/jetbrains_ext/BamlPluginTest.kt @@ -29,9 +29,9 @@ class BamlPluginTest : BasePlatformTestCase() { } fun testProjectService() { - val projectService = project.service() - - assertNotSame(projectService.getRandomNumber(), projectService.getRandomNumber()) +// val projectService = project.service() +// +// assertNotSame(projectService.getRandomNumber(), projectService.getRandomNumber()) } override fun getTestDataPath() = "src/test/testData/rename" diff --git a/typescript/playground-common/src/baml_wasm_web/EventListener.tsx b/typescript/playground-common/src/baml_wasm_web/EventListener.tsx index 59e1750f38..918e979b85 100644 --- a/typescript/playground-common/src/baml_wasm_web/EventListener.tsx +++ b/typescript/playground-common/src/baml_wasm_web/EventListener.tsx @@ -83,7 +83,7 @@ export const isConnectedAtom = atom(true) const ConnectionStatus: React.FC = () => { const isConnected = useAtomValue(isConnectedAtom) - if (isConnected || vscode.isVscode()) return null + if (isConnected) return null return (
@@ -108,7 +108,6 @@ export const EventListener: React.FC<{ children: React.ReactNode }> = ({ childre const debouncedSetFiles = useDebounceCallback(setFiles, 50, true) const setFlashRanges = useSetAtom(flashRangesAtom) const setIsConnected = useSetAtom(isConnectedAtom) - const isVSCodeWebview = vscode.isVscode() const [selectedFunc, setSelectedFunction] = useAtom(selectedFunctionAtom) const setSelectedTestcase = useSetAtom(selectedTestcaseAtom) @@ -135,15 +134,38 @@ export const EventListener: React.FC<{ children: React.ReactNode }> = ({ childre setOrchestratorIndex(0) } }, [selectedFunc]) - console.log('selectedFunc', selectedFunc) + // console.log('selectedFunc', selectedFunc) useEffect(() => { - // Only open websocket if not in VSCode webview - if (isVSCodeWebview) { + const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws' + const ws = new WebSocket(`${scheme}://${window.location.host}/ws`) + + ws.onopen = () => { + console.log('WebSocket Opened') setIsConnected(true) - // return + } + ws.onmessage = (e) => { + console.log('Websocket recieved message!') + try { + const payload = JSON.parse(e.data) + window.postMessage(payload, '*') + } catch (err) { + console.error('invalid WS payload', err) + } + } + ws.onclose = () => { + console.log('WebSocket Closed') + setIsConnected(false) + } + ws.onerror = () => { + console.error('WebSocket error') + setIsConnected(false) } + return () => ws.close() + }, [setIsConnected]) + + useEffect(() => { const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws' const ws = new WebSocket(`${scheme}://${window.location.host}/ws`) @@ -170,7 +192,7 @@ export const EventListener: React.FC<{ children: React.ReactNode }> = ({ childre } return () => ws.close() - }, [setIsConnected, isVSCodeWebview]) + }, [setIsConnected]) console.log('Websocket execution finished') diff --git a/typescript/playground-common/src/shared/baml-project-panel/atoms.ts b/typescript/playground-common/src/shared/baml-project-panel/atoms.ts index 0a51e30f28..55768eb411 100644 --- a/typescript/playground-common/src/shared/baml-project-panel/atoms.ts +++ b/typescript/playground-common/src/shared/baml-project-panel/atoms.ts @@ -167,7 +167,7 @@ const playgroundPortAtom = unwrap( export const proxyUrlAtom = atom((get) => { const vscodeSettings = get(vscodeSettingsAtom) const port = get(playgroundPortAtom) - const proxyUrl = port && port !== 0 ? `http://localhost:${port}` : undefined + const proxyUrl = port && port !== 0 ? `http://localhost:${port + 1}` : undefined const proxyEnabled = !!vscodeSettings?.enablePlaygroundProxy return { proxyEnabled, @@ -296,7 +296,8 @@ const defaultEnvKeyValues: [string, string][] = (() => { } else { console.log('Not running in a Next.js environment, set default value') // Not running in a Next.js environment, set default value - return [['BOUNDARY_PROXY_URL', 'http://localhost:0000']] + // The proxy is now handled by the LSP, so we'll use a placeholder that will be replaced + return [['BOUNDARY_PROXY_URL', 'http://localhost:3031']] } })() export const envKeyValueStorage = atomWithStorage<[string, string][]>( diff --git a/typescript/playground-common/src/shared/baml-project-panel/playground-panel/atoms.ts b/typescript/playground-common/src/shared/baml-project-panel/playground-panel/atoms.ts index b15e8719f2..5d93fcb4bc 100644 --- a/typescript/playground-common/src/shared/baml-project-panel/playground-panel/atoms.ts +++ b/typescript/playground-common/src/shared/baml-project-panel/playground-panel/atoms.ts @@ -148,11 +148,6 @@ export const showEnvDialogAtom = atom( const hasShownDialog = get(hasShownEnvDialogAtom) if (hasShownDialog) return envDialogOpen - // if we are in vscode, we don't want to show the dialog - if (!vscode.isVscode()) { - return false - } - return hasMissingVars }, (get, set, value: boolean) => { @@ -165,8 +160,6 @@ export const showEnvDialogAtom = atom( export const areEnvVarsMissingAtom = atom((get) => { const requiredVars = get(requiredEnvVarsAtom) - const isVscode = vscode.isVscode() - if (!isVscode) return false const envVars = get(envVarsAtom) return requiredVars.length > 0 && requiredVars.every((key) => !envVars[key]) }) diff --git a/typescript/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/media-utils.ts b/typescript/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/media-utils.ts index bd089a9fa1..490eb80265 100644 --- a/typescript/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/media-utils.ts +++ b/typescript/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/media-utils.ts @@ -1,59 +1,31 @@ import { vscode } from '../../vscode' export const findMediaFile = async (path: string): Promise => { - // Helper to hash the path to ensure the same path generates the same data - // const hashPath = (str: string): number => { - // let hash = 0; - // for (let i = 0; i < str.length; i++) { - // hash = (hash << 5) - hash + str.charCodeAt(i); - // hash |= 0; - // } - // return Math.abs(hash); - // }; - - // // Helper to generate a dummy image in JPEG/PNG format with text - // const generateRandomImage = async ( - // type: 'image/jpeg' | 'image/png', - // width: number, - // height: number, - // text: string - // ): Promise => { - // const canvas = new OffscreenCanvas(width, height); - // const ctx = canvas.getContext('2d'); - // if (ctx) { - // const colorSeed = hashPath(text); - // ctx.fillStyle = `#${((colorSeed & 0xFFFFFF) | 0x1000000).toString(16).slice(1)}`; - // ctx.fillRect(0, 0, width, height); - - // ctx.fillStyle = '#FFFFFF'; - // ctx.font = '16px Arial'; - // ctx.fillText(`Placeholder: ${type}`, 10, 50); - // ctx.fillText(path, 10, 70); - // } - // const blob = await canvas.convertToBlob({ type }); - // return new Uint8Array(await blob.arrayBuffer()); - // }; - - // const generateRandomAudio = (path: string): Uint8Array => { - // const audioLength = 1 * 1024 * 1024; - // const randomAudioData = new Uint8Array(audioLength); - // const seed = hashPath(path); - // for (let i = 0; i < audioLength; i++) { - // randomAudioData[i] = (seed + i) % 256; - // } - // return randomAudioData; - // }; - - // const extension = path.split('.').pop()?.toLowerCase(); - // if (extension === 'jpeg' || extension === 'jpg') { - // return await generateRandomImage('image/jpeg', 200, 100, `Dummy JPEG:\n${path}`); - // } else if (extension === 'png') { - // return await generateRandomImage('image/png', 200, 100, `Dummy PNG:\n${path}`); - // } else if (extension === 'mp3') { - // return generateRandomAudio(path); - // } - - return await vscode.readFile(path) - - // throw new Error(`Unknown file extension: ${extension}`); + // Try to get the URI from the backend + const resp = await vscode.readLocalFile('', path) + if (resp.uri) { + // Fetch the file from the URI if provided + const res = await fetch(resp.uri) + if (!res.ok) { + throw new Error(`Failed to fetch file from URI: ${resp.uri}`) + } + const buffer = await res.arrayBuffer() + return new Uint8Array(buffer) + } + if (resp.readError) { + throw new Error(`Failed to read file: ${path}\n${resp.readError}`) + } + if (resp.contents) { + // Fallback: decode base64 contents + const contents = resp.contents + // Use atob to decode base64 to binary string, then to Uint8Array + const binary = atob(contents) + const len = binary.length + const bytes = new Uint8Array(len) + for (let i = 0; i < len; i++) { + bytes[i] = binary.charCodeAt(i) + } + return bytes + } + throw new Error(`Unknown error: '${path}'`) } diff --git a/typescript/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/webview-media.tsx b/typescript/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/webview-media.tsx index 08f4b8c12f..14663e161a 100644 --- a/typescript/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/webview-media.tsx +++ b/typescript/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/webview-media.tsx @@ -33,7 +33,7 @@ export const WebviewMedia: React.FC = ({ bamlMediaType, media switch (media.type) { case wasm.WasmChatMessagePartMediaType.File: - return `${media.content}` + return media.content case wasm.WasmChatMessagePartMediaType.Url: return media.content case wasm.WasmChatMessagePartMediaType.Error: @@ -64,16 +64,8 @@ export const WebviewMedia: React.FC = ({ bamlMediaType, media const img = e.currentTarget const { naturalWidth, naturalHeight } = img let size = 'Unknown' - if (mediaUrl?.startsWith('data:')) { - const base64Length = mediaUrl.split(',')[1]?.length - const sizeInBytes = base64Length ? base64Length * 0.75 : 0 - size = - sizeInBytes > 1048576 ? `${(sizeInBytes / 1048576).toFixed(2)} MB` : `${(sizeInBytes / 1024).toFixed(2)} KB` - } else { - const sizeInBytes = naturalWidth * naturalHeight * 4 - size = - sizeInBytes > 1048576 ? `${(sizeInBytes / 1048576).toFixed(2)} MB` : `${(sizeInBytes / 1024).toFixed(2)} KB` - } + const sizeInBytes = naturalWidth * naturalHeight * 4 + size = sizeInBytes > 1048576 ? `${(sizeInBytes / 1048576).toFixed(2)} MB` : `${(sizeInBytes / 1024).toFixed(2)} KB` setImageStats({ width: naturalWidth, height: naturalHeight, size }) } diff --git a/typescript/playground-common/src/shared/baml-project-panel/playground-panel/side-bar/index.tsx b/typescript/playground-common/src/shared/baml-project-panel/playground-panel/side-bar/index.tsx index 107550732b..7aad6a923b 100644 --- a/typescript/playground-common/src/shared/baml-project-panel/playground-panel/side-bar/index.tsx +++ b/typescript/playground-common/src/shared/baml-project-panel/playground-panel/side-bar/index.tsx @@ -53,7 +53,7 @@ const functionsAreStaleAtom = atom((get) => { const isEmbed = typeof window !== 'undefined' && window.location.href.includes('embed') -export const isSidebarOpenAtom = atomWithStorage('isSidebarOpen', isEmbed ? false : vscode.isVscode() ? true : false) +export const isSidebarOpenAtom = atomWithStorage('isSidebarOpen', isEmbed ? true : false) export default function CustomSidebar({ isEmbed = false }: { isEmbed?: boolean }) { const functions = useAtomValue(functionsAtom) diff --git a/typescript/playground-common/src/shared/baml-project-panel/vscode.ts b/typescript/playground-common/src/shared/baml-project-panel/vscode.ts index 142a3883b6..76ced42631 100644 --- a/typescript/playground-common/src/shared/baml-project-panel/vscode.ts +++ b/typescript/playground-common/src/shared/baml-project-panel/vscode.ts @@ -44,27 +44,49 @@ const isRpcResponse = (eventData: unknown): eventData is RpcResponse => { * enabled by acquireVsCodeApi. */ class VSCodeAPIWrapper { - private readonly vsCodeApi: WebviewApi | undefined - + private ws: WebSocket | undefined + private wsReady: Promise | undefined + private wsReadyResolve: (() => void) | undefined private rpcTable: Map void }> private rpcId: number + public isConnected: boolean = false + public onOpen?: () => void constructor() { - // Check if the acquireVsCodeApi function exists in the current development - // context (i.e. VS Code development window or web browser) - if (typeof acquireVsCodeApi === 'function' && typeof window !== 'undefined') { - this.vsCodeApi = acquireVsCodeApi() - window.addEventListener('message', this.listenForRpcResponses.bind(this)) + if (typeof window !== 'undefined') { + // Use robust WebSocket setup like EventListener + const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws' + const host = window.location.host + const url = `${scheme}://${host}/rpc` + this.wsReady = new Promise((resolve) => (this.wsReadyResolve = resolve)) + this.ws = new WebSocket(url) + this.ws.onopen = () => { + console.log('RPC WebSocket Opened') + this.isConnected = true + this.wsReadyResolve?.() + if (this.onOpen) this.onOpen() + } + this.ws.onclose = () => { + console.log('RPC WebSocket Closed') + this.isConnected = false + } + this.ws.onerror = (e) => { + console.error('RPC WebSocket error', e) + this.isConnected = false + } + this.ws.onmessage = (event) => { + console.log('RPC WebSocket message received:', event.data) + let data = event.data + try { + data = JSON.parse(event.data) + } catch {} + this.listenForRpcResponses({ data }) + } } - this.rpcTable = new Map() this.rpcId = 0 } - public isVscode() { - return this.vsCodeApi !== undefined - } - public async readFile(path: string): Promise { const uri = await this.readLocalFile('', path) @@ -73,7 +95,6 @@ class VSCodeAPIWrapper { } if (uri.contents) { const contents = uri.contents - // throw new Error(`not implemented: ${Array.isArray(contents)}: \n ${JSON.stringify(contents)}`) return decodeBuffer(contents) } @@ -173,57 +194,22 @@ class VSCodeAPIWrapper { } } - /** - * Post a message (i.e. send arbitrary data) to the owner of the webview. - * - * @remarks When running webview code inside a web browser, postMessage will instead - * log the given message to the console. - * - * @param message Abitrary data (must be JSON serializable) to send to the extension context. - */ public postMessage(message: unknown) { - if (this.vsCodeApi) { - this.vsCodeApi.postMessage(message) - } else { - window.postMessage(message) + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)) + } else if (this.ws && this.wsReady) { + this.wsReady.then(() => this.ws!.send(JSON.stringify(message))) } } - /** - * Get the persistent state stored for this webview. - * - * @remarks When running webview source code inside a web browser, getState will retrieve state - * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). - * - * @return The current state or `undefined` if no state has been set. - */ public getState(): unknown | undefined { - if (this.vsCodeApi) { - return this.vsCodeApi.getState() - } else { - const state = localStorage.getItem('vscodeState') - return state ? JSON.parse(state) : undefined - } + const state = localStorage.getItem('vscodeState') + return state ? JSON.parse(state) : undefined } - /** - * Set the persistent state stored for this webview. - * - * @remarks When running webview source code inside a web browser, setState will set the given - * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). - * - * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved - * using {@link getState}. - * - * @return The new state. - */ public setState(newState: T): T { - if (this.vsCodeApi) { - return this.vsCodeApi.setState(newState) - } else { - localStorage.setItem('vscodeState', JSON.stringify(newState)) - return newState - } + localStorage.setItem('vscodeState', JSON.stringify(newState)) + return newState } } diff --git a/typescript/pnpm-lock.yaml b/typescript/pnpm-lock.yaml index 2689b2da7b..8525e9584a 100644 --- a/typescript/pnpm-lock.yaml +++ b/typescript/pnpm-lock.yaml @@ -264,7 +264,7 @@ importers: version: 16.4.7 http-proxy: specifier: ^1.18.1 - version: 1.18.1(debug@4.4.0) + version: 1.18.1 jotai: specifier: ^2.8.0 version: 2.11.0(@types/react@18.3.18)(react@18.3.1) @@ -860,27 +860,15 @@ importers: comlink: specifier: ^4.4.2 version: 4.4.2 - cors: - specifier: ^2.8.5 - version: 2.8.5 dotenv: specifier: ^16.4.7 version: 16.4.7 env-paths: specifier: 2.2.1 version: 2.2.1 - express: - specifier: ^4.21.1 - version: 4.21.2 google-auth-library: specifier: ^9.15.1 version: 9.15.1 - http-proxy: - specifier: ^1.18.1 - version: 1.18.1(debug@4.4.0) - http-proxy-middleware: - specifier: ^3.0.3 - version: 3.0.3 minimatch: specifier: 6.2.0 version: 6.2.0 @@ -918,18 +906,9 @@ importers: '@types/adm-zip': specifier: ^0.5.7 version: 0.5.7 - '@types/cors': - specifier: ^2.8.17 - version: 2.8.17 - '@types/express': - specifier: ^4.17.21 - version: 4.17.21 '@types/glob': specifier: 8.1.0 version: 8.1.0 - '@types/http-proxy': - specifier: ^1.17.14 - version: 1.17.15 '@types/mocha': specifier: 10.0.3 version: 10.0.3 @@ -4759,15 +4738,6 @@ packages: '@types/base16@1.0.5': resolution: {integrity: sha512-OzOWrTluG9cwqidEzC/Q6FAmIPcnZfm8BFRlIx0+UIUqnuAmi5OS88O0RpT3Yz6qdmqObvUhasrbNsCofE4W9A==} - '@types/body-parser@1.19.5': - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/cors@2.8.17': - resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} - '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} @@ -4804,12 +4774,6 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/express-serve-static-core@4.19.6': - resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} - - '@types/express@4.17.21': - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} - '@types/glob@8.1.0': resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} @@ -4825,9 +4789,6 @@ packages: '@types/he@1.2.3': resolution: {integrity: sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==} - '@types/http-errors@2.0.4': - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - '@types/http-proxy@1.17.15': resolution: {integrity: sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==} @@ -4861,9 +4822,6 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -4891,12 +4849,6 @@ packages: '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/qs@6.9.17': - resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@18.3.5': resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} peerDependencies: @@ -4920,12 +4872,6 @@ packages: '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - '@types/send@0.17.4': - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - - '@types/serve-static@1.15.7': - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -5298,10 +5244,6 @@ packages: aborter@1.1.0: resolution: {integrity: sha512-9rHWMcWTEYsMB4l+ttgPujR7OiXH9NQbP0ej+SSVaK1e2yU/tePbYm8g/g9cQhJkgczp6lpEB2fdJYLKT/T0mg==} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -5461,9 +5403,6 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - array-includes@3.1.8: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} @@ -5771,10 +5710,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -5850,10 +5785,6 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -6129,24 +6060,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} - copy-descriptor@0.1.1: resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} engines: {node: '>=0.10.0'} @@ -6168,10 +6084,6 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - cpx@1.5.0: resolution: {integrity: sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA==} hasBin: true @@ -6398,18 +6310,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -6499,9 +6403,6 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -6523,14 +6424,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - encoding-sniffer@0.2.0: resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} @@ -6627,9 +6520,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -6803,10 +6693,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -6848,10 +6734,6 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.0'} - extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -6936,10 +6818,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} - engines: {node: '>= 0.8'} - find-index@0.1.1: resolution: {integrity: sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==} @@ -7001,10 +6879,6 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -7026,10 +6900,6 @@ packages: react-dom: optional: true - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -7323,10 +7193,6 @@ packages: htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - http-proxy-agent@4.0.1: resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} engines: {node: '>= 6'} @@ -7335,10 +7201,6 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} - http-proxy-middleware@3.0.3: - resolution: {integrity: sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - http-proxy@1.18.1: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} @@ -7355,10 +7217,6 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -7413,10 +7271,6 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-accessor-descriptor@1.0.1: resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} engines: {node: '>= 0.10'} @@ -7633,10 +7487,6 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} - is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - is-posix-bracket@0.1.1: resolution: {integrity: sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ==} engines: {node: '>=0.10.0'} @@ -8397,19 +8247,12 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} memoizerific@1.11.3: resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -8417,10 +8260,6 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - micromark-core-commonmark@2.0.2: resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} @@ -8680,10 +8519,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -8843,10 +8678,6 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -8940,10 +8771,6 @@ packages: parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - pascalcase@0.1.1: resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} engines: {node: '>=0.10.0'} @@ -8971,9 +8798,6 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -9156,10 +8980,6 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -9177,10 +8997,6 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - qs@6.13.1: resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} engines: {node: '>=0.6'} @@ -9195,14 +9011,6 @@ packages: randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} - rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -9759,10 +9567,6 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} - seq@0.3.5: resolution: {integrity: sha512-sisY2Ln1fj43KBkRtXkesnRHYNdswIkIibvNe/0UKm2GZxjMbqmccpiatoKr/k2qX5VKiLU8xm+tz/74LAho4g==} @@ -9772,10 +9576,6 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -9795,9 +9595,6 @@ packages: setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} @@ -9942,10 +9739,6 @@ packages: resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} engines: {node: '>=0.10.0'} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} @@ -10258,10 +10051,6 @@ packages: resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} engines: {node: '>=0.10.0'} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -10458,10 +10247,6 @@ packages: resolution: {integrity: sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==} engines: {node: '>=16'} - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -10577,10 +10362,6 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - unplugin@1.16.1: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} @@ -10677,10 +10458,6 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -10715,10 +10492,6 @@ packages: validate.io-number@1.0.3: resolution: {integrity: sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==} - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -15244,19 +15017,6 @@ snapshots: '@types/base16@1.0.5': {} - '@types/body-parser@1.19.5': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 20.17.12 - - '@types/connect@3.4.38': - dependencies: - '@types/node': 20.17.12 - - '@types/cors@2.8.17': - dependencies: - '@types/node': 20.17.12 - '@types/d3-color@3.1.3': {} '@types/d3-drag@3.0.7': @@ -15300,20 +15060,6 @@ snapshots: '@types/estree@1.0.6': {} - '@types/express-serve-static-core@4.19.6': - dependencies: - '@types/node': 20.17.12 - '@types/qs': 6.9.17 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - - '@types/express@4.17.21': - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.19.6 - '@types/qs': 6.9.17 - '@types/serve-static': 1.15.7 - '@types/glob@8.1.0': dependencies: '@types/minimatch': 5.1.2 @@ -15333,8 +15079,6 @@ snapshots: '@types/he@1.2.3': {} - '@types/http-errors@2.0.4': {} - '@types/http-proxy@1.17.15': dependencies: '@types/node': 20.17.12 @@ -15368,8 +15112,6 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/mime@1.3.5': {} - '@types/minimatch@5.1.2': {} '@types/mocha@10.0.3': {} @@ -15394,10 +15136,6 @@ snapshots: '@types/prop-types@15.7.14': {} - '@types/qs@6.9.17': {} - - '@types/range-parser@1.2.7': {} - '@types/react-dom@18.3.5(@types/react@18.3.18)': dependencies: '@types/react': 18.3.18 @@ -15423,17 +15161,6 @@ snapshots: '@types/semver@7.5.8': {} - '@types/send@0.17.4': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.17.12 - - '@types/serve-static@1.15.7': - dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 20.17.12 - '@types/send': 0.17.4 - '@types/stack-utils@2.0.3': {} '@types/stylis@4.2.5': {} @@ -16056,11 +15783,6 @@ snapshots: aborter@1.1.0: {} - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -16200,8 +15922,6 @@ snapshots: call-bound: 1.0.3 is-array-buffer: 3.0.5 - array-flatten@1.1.1: {} - array-includes@3.1.8: dependencies: call-bind: 1.0.8 @@ -16307,7 +16027,7 @@ snapshots: axios@1.7.9: dependencies: - follow-redirects: 1.15.9(debug@4.4.0) + follow-redirects: 1.15.9 form-data: 4.0.1 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16741,23 +16461,6 @@ snapshots: readable-stream: 3.6.2 optional: true - body-parser@1.20.3: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - boolbase@1.0.0: {} bottleneck@2.19.5: {} @@ -16855,8 +16558,6 @@ snapshots: dependencies: streamsearch: 1.1.0 - bytes@3.1.2: {} - cac@6.7.14: {} cache-base@1.0.1: @@ -17180,18 +16881,8 @@ snapshots: concat-map@0.0.1: {} - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} - convert-source-map@2.0.0: {} - cookie-signature@1.0.6: {} - - cookie@0.7.1: {} - copy-descriptor@0.1.1: {} copyfiles@2.4.1: @@ -17215,11 +16906,6 @@ snapshots: core-util-is@1.0.3: {} - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - cpx@1.5.0: dependencies: babel-runtime: 6.26.0 @@ -17463,12 +17149,8 @@ snapshots: delayed-stream@1.0.0: {} - depd@2.0.0: {} - dequal@2.0.3: {} - destroy@1.2.0: {} - detect-libc@2.0.3: optional: true @@ -17550,8 +17232,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - ee-first@1.1.1: {} - ejs@3.1.10: dependencies: jake: 10.9.2 @@ -17566,10 +17246,6 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@1.0.2: {} - - encodeurl@2.0.0: {} - encoding-sniffer@0.2.0: dependencies: iconv-lite: 0.6.3 @@ -17824,8 +17500,6 @@ snapshots: escalade@3.2.0: {} - escape-html@1.0.3: {} - escape-string-regexp@1.0.5: {} escape-string-regexp@2.0.0: {} @@ -18088,8 +17762,6 @@ snapshots: esutils@2.0.3: {} - etag@1.8.1: {} - eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} @@ -18143,42 +17815,6 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - express@4.21.2: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.12 - proxy-addr: 2.0.7 - qs: 6.13.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -18282,18 +17918,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.1: - dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - find-index@0.1.1: {} find-up@4.1.0: @@ -18316,9 +17940,7 @@ snapshots: flatted@3.3.2: {} - follow-redirects@1.15.9(debug@4.4.0): - optionalDependencies: - debug: 4.4.0 + follow-redirects@1.15.9: {} for-each@0.3.3: dependencies: @@ -18349,8 +17971,6 @@ snapshots: dependencies: fetch-blob: 3.2.0 - forwarded@0.2.0: {} - fraction.js@4.3.7: {} fragment-cache@0.2.1: @@ -18367,8 +17987,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - fresh@0.5.2: {} - fs-constants@1.0.0: optional: true @@ -18751,14 +18369,6 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - http-proxy-agent@4.0.1: dependencies: '@tootallnate/once': 1.1.2 @@ -18774,21 +18384,10 @@ snapshots: transitivePeerDependencies: - supports-color - http-proxy-middleware@3.0.3: - dependencies: - '@types/http-proxy': 1.17.15 - debug: 4.4.0 - http-proxy: 1.18.1(debug@4.4.0) - is-glob: 4.0.3 - is-plain-object: 5.0.0 - micromatch: 4.0.8 - transitivePeerDependencies: - - supports-color - - http-proxy@1.18.1(debug@4.4.0): + http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.9(debug@4.4.0) + follow-redirects: 1.15.9 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -18809,10 +18408,6 @@ snapshots: human-signals@2.1.0: {} - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -18862,8 +18457,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - ipaddr.js@1.9.1: {} - is-accessor-descriptor@1.0.1: dependencies: hasown: 2.0.2 @@ -19056,8 +18649,6 @@ snapshots: dependencies: isobject: 3.0.1 - is-plain-object@5.0.0: {} - is-posix-bracket@0.1.1: {} is-primitive@2.0.0: {} @@ -20189,22 +19780,16 @@ snapshots: mdurl@2.0.0: {} - media-typer@0.3.0: {} - memoize-one@5.2.1: {} memoizerific@1.11.3: dependencies: map-or-similar: 1.5.0 - merge-descriptors@1.0.3: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} - methods@1.1.2: {} - micromark-core-commonmark@2.0.2: dependencies: decode-named-character-reference: 1.0.2 @@ -20615,8 +20200,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@0.6.3: {} - neo-async@2.6.2: {} next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -20783,10 +20366,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -20831,7 +20410,7 @@ snapshots: dependencies: '@vscode/vsce': 2.21.1 commander: 6.2.1 - follow-redirects: 1.15.9(debug@4.4.0) + follow-redirects: 1.15.9 is-ci: 2.0.0 leven: 3.1.0 semver: 7.6.3 @@ -20923,8 +20502,6 @@ snapshots: dependencies: entities: 4.5.0 - parseurl@1.3.3: {} - pascalcase@0.1.1: {} path-exists@4.0.0: {} @@ -20945,8 +20522,6 @@ snapshots: lru-cache: 11.0.2 minipass: 7.1.2 - path-to-regexp@0.1.12: {} - path-type@4.0.0: {} pathval@2.0.0: {} @@ -21114,11 +20689,6 @@ snapshots: property-information@6.5.0: {} - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - proxy-from-env@1.1.0: {} pump@3.0.2: @@ -21133,10 +20703,6 @@ snapshots: pure-rand@6.1.0: {} - qs@6.13.0: - dependencies: - side-channel: 1.1.0 - qs@6.13.1: dependencies: side-channel: 1.1.0 @@ -21153,15 +20719,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - range-parser@1.2.1: {} - - raw-body@2.5.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -21920,24 +21477,6 @@ snapshots: semver@7.6.3: {} - send@0.19.0: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - seq@0.3.5: dependencies: chainsaw: 0.0.9 @@ -21951,15 +21490,6 @@ snapshots: dependencies: randombytes: 2.1.0 - serve-static@1.16.2: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.0 - transitivePeerDependencies: - - supports-color - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -21991,8 +21521,6 @@ snapshots: setimmediate@1.0.5: {} - setprototypeof@1.2.0: {} - shallowequal@1.1.0: {} sharp@0.33.5: @@ -22176,8 +21704,6 @@ snapshots: define-property: 0.2.5 object-copy: 0.1.0 - statuses@2.0.1: {} - stoppable@1.1.0: {} store2@2.14.4: {} @@ -22593,8 +22119,6 @@ snapshots: regex-not: 1.0.2 safe-regex: 1.1.0 - toidentifier@1.0.1: {} - tr46@0.0.3: {} tr46@1.0.1: @@ -22802,11 +22326,6 @@ snapshots: type-fest@4.31.0: {} - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.3 @@ -22947,8 +22466,6 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - unpipe@1.0.0: {} - unplugin@1.16.1: dependencies: acorn: 8.14.0 @@ -23065,8 +22582,6 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.18 - utils-merge@1.0.1: {} - uuid@10.0.0: {} uuid@8.3.2: {} @@ -23096,8 +22611,6 @@ snapshots: validate.io-number@1.0.3: {} - vary@1.1.2: {} - vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 diff --git a/typescript/vscode-ext/packages/vscode/package.json b/typescript/vscode-ext/packages/vscode/package.json index 2c4e542307..f26f7645a9 100644 --- a/typescript/vscode-ext/packages/vscode/package.json +++ b/typescript/vscode-ext/packages/vscode/package.json @@ -41,13 +41,9 @@ "adm-zip": "^0.5.16", "axios": "^1.7.9", "comlink": "^4.4.2", - "cors": "^2.8.5", "dotenv": "^16.4.7", "env-paths": "2.2.1", - "express": "^4.21.1", "google-auth-library": "^9.15.1", - "http-proxy": "^1.18.1", - "http-proxy-middleware": "^3.0.3", "minimatch": "6.2.0", "node-fetch": "^3.3.2", "posthog-node": "^3.2.1", @@ -62,10 +58,7 @@ }, "devDependencies": { "@types/adm-zip": "^0.5.7", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", "@types/glob": "8.1.0", - "@types/http-proxy": "^1.17.14", "@types/mocha": "10.0.3", "@types/vscode": "1.63.0", "@vscode/test-electron": "2.3.5", diff --git a/typescript/vscode-ext/packages/vscode/src/extension.ts b/typescript/vscode-ext/packages/vscode/src/extension.ts index 547bc2360b..5b269a6dba 100644 --- a/typescript/vscode-ext/packages/vscode/src/extension.ts +++ b/typescript/vscode-ext/packages/vscode/src/extension.ts @@ -1,29 +1,23 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ import * as vscode from 'vscode' -import axios from 'axios' import glooLens from './LanguageToBamlCodeLensProvider' -import { WebviewPanelHost, openPlaygroundConfig } from './panels/WebviewPanelHost' +import { WebviewPanelHost } from './panels/WebviewPanelHost' import plugins from './plugins' -import { requestBamlCLIVersion, requestDiagnostics } from './plugins/language-server-client' -import { telemetry } from './plugins/language-server-client' -import cors from 'cors' -import { createProxyMiddleware } from 'http-proxy-middleware' +import { requestBamlCLIVersion, requestDiagnostics, getPlaygroundPort } from './plugins/language-server-client' +import { telemetry, viewFunctionInPlayground, runTestInPlayground } from './plugins/language-server-client' +import StatusBarPanel from './panels/StatusBarPanel' +import TelemetryReporter from './telemetryReporter' const outputChannel = vscode.window.createOutputChannel('baml') const diagnosticsCollection = vscode.languages.createDiagnosticCollection('baml-diagnostics') const LANG_NAME = 'Baml' -let server: any let glowOnDecoration: vscode.TextEditorDecorationType | null = null let glowOffDecoration: vscode.TextEditorDecorationType | null = null let isGlowOn: boolean = true let animationTimer: NodeJS.Timeout | null = null let highlightRanges: vscode.Range[] = [] -import type { Express } from 'express' -import StatusBarPanel from './panels/StatusBarPanel' -import { Socket } from 'net' - export function activate(context: vscode.ExtensionContext) { console.log('BAML extension activating') @@ -35,127 +29,35 @@ export function activate(context: vscode.ExtensionContext) { createDecorations() startAnimation() - const app: Express = require('express')() - app.use(cors()) - const server = app.listen(0, () => { - console.log('Server started on port ' + getPort()) - WebviewPanelHost.currentPanel?.postMessage('port_number', { - port: getPort(), - }) - }) - + // Wrapper function to handle null case from getPlaygroundPort const getPort = () => { - const addr = server.address() - if (addr === null) { - vscode.window.showErrorMessage( - 'Failed to start BAML extension server. Please try reloading the window, or restarting VSCode.', - ) - console.error('Failed to start BAML extension server. Please try reloading the window, or restarting VSCode.') - return 0 - } - if (typeof addr === 'string') { - return parseInt(addr) - } - return addr.port + const port = getPlaygroundPort() + return port ?? 3030 // Default to 3030 if null } - app.use( - createProxyMiddleware({ - changeOrigin: true, // leave prependPath = true (default) - /** Inspect and (maybe) rewrite the path. */ - pathRewrite: (path, req) => { - // If the path looks like an image (xyz.png …) and it's a GET → blank it. - if (/\.[a-z0-9]+$/i.test(path) && req.method === 'GET') { - console.log('[PROXY] Image request detected, clearing path:', path) - return '' - } - - // Remove trailing slash so we don't end up with "//". - const out = path.endsWith('/') ? path.slice(0, -1) : path - return out - }, - - /** Dynamically choose target and massage req.url. */ - router: (req) => { - const raw = req.headers['baml-original-url'] - if (typeof raw !== 'string') { - throw new Error('missing baml-original-url header') - } - - // Clean up headers the upstream may reject - delete req.headers['baml-original-url'] - delete req.headers['origin'] - - // Strip trailing slash on header value, then parse - const cleanRaw = raw.endsWith('/') ? raw.slice(0, -1) : raw - const url = new URL(cleanRaw) - - // Base path to prepend *if necessary* - const basePath = url.pathname.replace(/\/$/, '') // '/compat/v1' → '/compat/v1' - if (!req.url) { - throw new Error('missing req.url') - } - - // Guard against double-prefixing - if (basePath && !req.url.startsWith(basePath)) { - // Ensure there's exactly one slash between basePath and existing path - req.url = basePath + (req.url.startsWith('/') ? '' : '/') + req.url - } - - console.log('[PROXY]', req.method, req.url, '→', url.origin) - - // Tell HPM to proxy to the origin only (scheme + host) - return url.origin // e.g. "https://api.llama.com" - }, - - logger: console, - - on: { - /** Add CORS header. */ - proxyRes: (proxyRes, req) => { - proxyRes.headers['access-control-allow-origin'] = '*' - console.log('[PROXY]', req.method, req.url, '←', proxyRes.statusCode) - }, - - /** Robust error reporter with type-guard. */ - error: (err, req, res) => { - console.error('[PROXY ERROR]', req.method, req.url, ':', err.message) - - if ('writeHead' in res) { - const svr = res - if (!svr.headersSent) { - svr.writeHead(500, { 'content-type': 'application/json' }) - } - svr.end(JSON.stringify({ error: err.message })) - } else if (res instanceof Socket) { - res.destroy() - } - }, - }, - }), - ) - const bamlPlaygroundCommand = vscode.commands.registerCommand( 'baml.openBamlPanel', - (args?: { projectId?: string; functionName?: string; implName?: string; showTests?: boolean }) => { + async (args?: { projectId?: string; functionName?: string; implName?: string; showTests?: boolean }) => { const config = vscode.workspace.getConfiguration() config.update('baml.bamlPanelOpen', true, vscode.ConfigurationTarget.Global) - WebviewPanelHost.render(context.extensionUri, getPort, telemetry) + if (telemetry) { telemetry.sendTelemetryEvent({ event: 'baml.openBamlPanel', properties: {}, }) } - // sends project files as well to webview - requestDiagnostics() - openPlaygroundConfig.lastOpenedFunction = args?.functionName ?? 'default' - WebviewPanelHost.currentPanel?.postMessage('select_function', { - root_path: 'default', - function_name: args?.functionName ?? 'default', - }) + // Call the LSP to change function if a function name is provided + if (args?.functionName) { + try { + await viewFunctionInPlayground(args) + } catch (error) { + console.error('Failed to notify LSP of function change:', error) + // Continue execution even if LSP notification fails + } + } console.info('Opening BAML panel') }, @@ -163,7 +65,7 @@ export function activate(context: vscode.ExtensionContext) { const bamlTestcaseCommand = vscode.commands.registerCommand( 'baml.runBamlTest', - (args?: { + async (args?: { projectId: string functionName?: string implName?: string @@ -178,19 +80,19 @@ export function activate(context: vscode.ExtensionContext) { }) } + // Call the LSP to run test if test case name is provided + if (args?.testCaseName && args?.functionName && args?.projectId) { + try { + await runTestInPlayground(args) + } catch (error) { + console.error('Failed to notify LSP of test run:', error) + // Continue execution even if LSP notification fails + } + } + // sends project files as well to webview requestDiagnostics() - openPlaygroundConfig.lastOpenedFunction = args?.functionName ?? 'default' - WebviewPanelHost.currentPanel?.postMessage('select_function', { - root_path: 'default', - function_name: args?.functionName ?? 'default', - }) - - WebviewPanelHost.currentPanel?.postMessage('run_test', { - test_name: args?.testCaseName ?? 'default', - }) - console.info('Opening BAML panel') }, ) @@ -266,14 +168,7 @@ export function activate(context: vscode.ExtensionContext) { const text = editor.document.getText() // TODO: buggy when used with multiple functions, needs a fix. - WebviewPanelHost.currentPanel?.postMessage('update_cursor', { - cursor: { - fileName: name, - fileText: text, - line: position.line + 1, - column: position.character, - }, - }) + // Cursor position tracking removed since we're using iframe approach } } }) @@ -325,7 +220,7 @@ export function deactivate(): void { void plugin.deactivate() } } - server?.close() + // server?.close() } // Create our two decoration states diff --git a/typescript/vscode-ext/packages/vscode/src/panels/WebviewPanelHost.ts b/typescript/vscode-ext/packages/vscode/src/panels/WebviewPanelHost.ts index 8c72c4651b..ffcb1fb697 100644 --- a/typescript/vscode-ext/packages/vscode/src/panels/WebviewPanelHost.ts +++ b/typescript/vscode-ext/packages/vscode/src/panels/WebviewPanelHost.ts @@ -1,47 +1,13 @@ -import type { StringSpan } from '@baml/common' -import { fromIni } from '@aws-sdk/credential-providers' // ES6 import import { type Disposable, Uri, ViewColumn, type Webview, type WebviewPanel, window, workspace } from 'vscode' import * as vscode from 'vscode' import { getNonce } from '../utils/getNonce' -import { getUri } from '../utils/getUri' -import { - EchoResponse, - GetBamlSrcResponse, - LoadEnvRequest, - GetPlaygroundPortResponse, - GetVSCodeSettingsResponse, - GetWebviewUriResponse, - WebviewToVscodeRpc, - encodeBuffer, - LoadEnvResponse, -} from '../vscode-rpc' - -import { type Config, adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator' -import { URI } from 'vscode-uri' -import { getCurrentOpenedFile } from '../helpers/get-open-file' -import { bamlConfig, requestDiagnostics } from '../plugins/language-server-client' +import { getPlaygroundPort } from '../plugins/language-server-client' import TelemetryReporter from '../telemetryReporter' -import { exec, fork } from 'child_process' -import { promisify } from 'util' -import { dirname, join } from 'path' -import * as dotenv from 'dotenv' -import * as fs from 'fs' -import { AwsCredentialIdentity } from '@smithy/types' -import { refreshBamlConfigSingleton } from '../plugins/language-server-client/bamlConfig' -import { GoogleAuth } from 'google-auth-library' -// import { CredentialsProviderError } from '@aws-sdk/credential-providers' -const customConfig: Config = { - dictionaries: [adjectives, colors, animals], - separator: '_', - length: 2, -} -export const openPlaygroundConfig: { lastOpenedFunction: null | string } = { - lastOpenedFunction: null, -} +const packageJson = require('../../../package.json') // eslint-disable-line -const execAsync = promisify(exec) -const readFileAsync = promisify(fs.readFile) +// Manual debug toggle - set to true for debug mode, false for production +const DEBUG_MODE = false /** * This class manages the state and behavior of HelloWorld webview panels. @@ -58,6 +24,14 @@ export class WebviewPanelHost { private readonly _panel: WebviewPanel private _disposables: Disposable[] = [] private _port: () => number + private _playgroundPort: number | null = null + + /** + * Gets the current playground port + */ + public get playgroundPort(): number | null { + return this._playgroundPort + } /** * The WebPanelView class private constructor (called only from the render method). @@ -78,11 +52,279 @@ export class WebviewPanelHost { // the panel or when the panel is closed programmatically) this._panel.onDidDispose(() => this.dispose(), null, this._disposables) - // Set the HTML content for the webview panel - this._panel.webview.html = this._getWebviewContent(this._panel.webview, extensionUri) + // Show initial loading state + this._showLoadingState() + } + + /** + * Updates the playground port and refreshes the webview + */ + public updatePlaygroundPort(port: number) { + console.log(`WebviewPanelHost: Updating playground port to ${port}`) + this._playgroundPort = port + this._updateWebviewContent() + } + + /** + * Shows the loading state while waiting for the LSP port + */ + private _showLoadingState() { + this._panel.webview.html = ` + + + + + +
+
+
BAML Playground
+
Waiting on Baml language server...
+ ${ + DEBUG_MODE + ? ` +
+
Debug Mode: Active
+
Waiting for LSP port notification
+
Extension Version: ${packageJson.version}
+
+ ` + : '' + } +
+ + ` + } + + /** + * Updates the webview content with the playground iframe + */ + private _updateWebviewContent() { + if (!this._playgroundPort) { + // Still waiting for port from LSP + return + } - // Set an event listener to listen for messages passed from the webview context - this._setWebviewMessageListener(this._panel.webview) + const nonce = getNonce() + + this._panel.webview.html = ` + + + + + + + + + +
+
+
+ BAML Playground (LSP Mode) +
+
Port: ${this._playgroundPort}
+
+
+
+

Connecting to playground...

+ ${ + DEBUG_MODE + ? ` +

http://localhost:${this._playgroundPort}

+
+
Debug Mode: Active
+
Connected to port: ${this._playgroundPort}
+
Extension Version: ${packageJson.version}
+
+ ` + : '' + } +
+ +
+ + + + ` } /** @@ -95,6 +337,12 @@ export class WebviewPanelHost { if (WebviewPanelHost.currentPanel) { // If the webview panel already exists reveal it WebviewPanelHost.currentPanel._panel.reveal(ViewColumn.Beside) + + // Check if we have a port from LSP and update if needed + const currentPort = getPlaygroundPort() + if (currentPort && !WebviewPanelHost.currentPanel.playgroundPort) { + WebviewPanelHost.currentPanel.updatePlaygroundPort(currentPort) + } } else { // If a webview panel does not already exist create and show a new one const panel = window.createWebviewPanel( @@ -123,15 +371,13 @@ export class WebviewPanelHost { ) WebviewPanelHost.currentPanel = new WebviewPanelHost(panel, extensionUri, portLoader, reporter) - } - } - public postMessage(command: string, content: T) { - this._panel.webview.postMessage({ command: command, content }) - this.reporter?.sendTelemetryEvent({ - event: `baml.webview.${command}`, - properties: {}, - }) + // Check if we already have a port from LSP and update immediately + const currentPort = getPlaygroundPort() + if (currentPort) { + WebviewPanelHost.currentPanel.updatePlaygroundPort(currentPort) + } + } } /** @@ -140,314 +386,17 @@ export class WebviewPanelHost { public dispose() { WebviewPanelHost.currentPanel = undefined - // Dispose of the current webview panel + // Clean up our resources this._panel.dispose() const config = workspace.getConfiguration() config.update('baml.bamlPanelOpen', false, true) - // Dispose of all disposables (i.e. commands) for the current webview panel while (this._disposables.length) { - const disposable = this._disposables.pop() - if (disposable) { - disposable.dispose() + const x = this._disposables.pop() + if (x) { + x.dispose() } } } - - /** - * Defines and returns the HTML that should be rendered within the webview panel. - * - * @remarks This is also the place where references to the React webview dist files - * are created and inserted into the webview HTML. - * - * @param webview A reference to the extension webview - * @param extensionUri The URI of the directory containing the extension - * @returns A template string literal containing the HTML that should be - * rendered within the webview panel - */ - private _getWebviewContent(webview: Webview, extensionUri: Uri) { - // The CSS file from the React dist output - const stylesUri = getUri(webview, extensionUri, ['web-panel', 'dist', 'assets', 'index.css']) - // The JS file from the React dist output - const scriptUri = getUri(webview, extensionUri, ['web-panel', 'dist', 'assets', 'index.js']) - - const nonce = getNonce() - - // Tip: Install the es6-string-html VS Code extension to enable code highlighting below - return /*html*/ ` - - - - - - - Hello World - - -
Waiting for react: ${scriptUri}
- - - ` - } - - /** - * Sets up an event listener to listen for messages passed from the webview context and - * executes code based on the message that is recieved. - * - * @param webview A reference to the extension webview - * @param context A reference to the extension context - */ - private _setWebviewMessageListener(webview: Webview) { - console.log('_setWebviewMessageListener') - - const addProject = async () => { - await requestDiagnostics() - console.log('last opened func', openPlaygroundConfig.lastOpenedFunction) - this.postMessage('select_function', { - root_path: 'default', - function_name: openPlaygroundConfig.lastOpenedFunction, - }) - this.postMessage('baml_cli_version', bamlConfig.cliVersion) - this.postMessage('baml_settings_updated', bamlConfig) - } - - vscode.workspace.onDidChangeConfiguration((event) => { - console.log('*** CLIENT DID CHANGE CONFIGURATION', event) - if (event.affectsConfiguration('baml')) { - setTimeout(() => { - this.postMessage('baml_settings_updated', refreshBamlConfigSingleton()) - }, 1000) - } - }) - - webview.onDidReceiveMessage( - async ( - message: - | { - command: 'get_port' | 'add_project' | 'cancelTestRun' | 'removeTest' - } - | { - command: 'set_flashing_regions' - content: { - spans: { - file_path: string - start_line: number - start_char: number - end_line: number - end_char: number - }[] - } - } - | { - command: 'jumpToFile' - span: StringSpan - } - | { - command: 'telemetry' - meta: { - action: string - data: Record - } - } - | { - rpcId: number - data: WebviewToVscodeRpc - }, - ) => { - console.log('DEBUG: webview message: ', message) - if ('command' in message) { - switch (message.command) { - case 'add_project': - console.log('webview add_project') - addProject() - - return - case 'jumpToFile': { - try { - console.log('jumpToFile', message.span) - const span = message.span - // span.source_file is a file:/// URI - - const uri = vscode.Uri.parse(span.source_file) - await vscode.workspace.openTextDocument(uri).then((doc) => { - const range = new vscode.Range(doc.positionAt(span.start), doc.positionAt(span.end)) - vscode.window.showTextDocument(doc, { selection: range, viewColumn: ViewColumn.One }) - }) - } catch (e: any) { - console.log(e) - } - return - } - case 'telemetry': { - const { action, data } = message.meta - this.reporter?.sendTelemetryEvent({ - event: `baml.webview.${action}`, - properties: data, - }) - return - } - case 'set_flashing_regions': { - // Call the command handler with the spans - console.log('WEBPANELVIEW set_flashing_regions', message.content.spans) - vscode.commands.executeCommand('baml.setFlashingRegions', { content: message.content }) - return - } - } - } - - if (!('rpcId' in message)) { - return - } - - // console.log('message from webview, after above handlers:', message) - const vscodeMessage = message.data - const vscodeCommand = vscodeMessage.vscodeCommand - - // TODO: implement error handling in our RPC framework - switch (vscodeCommand) { - case 'ECHO': - const echoresp: EchoResponse = { message: vscodeMessage.message } - // also respond with rpc id - this._panel.webview.postMessage({ rpcId: message.rpcId, rpcMethod: vscodeCommand, data: echoresp }) - return - case 'SET_PROXY_SETTINGS': - const { proxyEnabled } = vscodeMessage - const config = vscode.workspace.getConfiguration() - config.update('baml.enablePlaygroundProxy', proxyEnabled, vscode.ConfigurationTarget.Workspace) - return - case 'GET_WEBVIEW_URI': - console.log('GET_WEBVIEW_URI', vscodeMessage) - // This is 1:1 with the contents of `image.file` in a test file, e.g. given `image { file baml_src://path/to-image.png }`, - // relpath will be 'baml_src://path/to-image.png' - const relpath = vscodeMessage.path - - // NB(san): this is a violation of the "never URI.parse rule" - // (see https://www.notion.so/gloochat/windows-uri-treatment-fe87b22abebb4089945eb8cd1ad050ef) - // but this relpath is already a file URI, it seems... - const uriPath = Uri.parse(relpath) - const uri = this._panel.webview.asWebviewUri(uriPath).toString() - - console.log('GET_WEBVIEW_URI', { vscodeMessage, uri, parsed: uriPath }) - let webviewUriResp: GetWebviewUriResponse = { - uri, - } - if (vscodeMessage.contents) { - try { - const contents = await workspace.fs.readFile(uriPath) - webviewUriResp = { - ...webviewUriResp, - contents: encodeBuffer(contents), - } - } catch (e) { - webviewUriResp = { - ...webviewUriResp, - readError: `${e}`, - } - } - } - this._panel.webview.postMessage({ rpcId: message.rpcId, rpcMethod: vscodeCommand, data: webviewUriResp }) - return - case 'GET_PLAYGROUND_PORT': - console.log('GET_PLAYGROUND_PORT', this._port(), Date.now()) - const response: GetPlaygroundPortResponse = { - port: this._port(), - } - this._panel.webview.postMessage({ rpcId: message.rpcId, rpcMethod: vscodeCommand, data: response }) - return - case 'LOAD_AWS_CREDS': - ;(async () => { - try { - const profile = vscodeMessage.profile - const credentialProvider = fromIni({ - profile: profile ?? undefined, - }) - const awsCreds = await credentialProvider() - this._panel.webview.postMessage({ - rpcId: message.rpcId, - rpcMethod: vscodeCommand, - data: { ok: awsCreds }, - }) - } catch (error) { - console.error('Error loading aws creds:', error) - if (error instanceof Error) { - this._panel.webview.postMessage({ - rpcId: message.rpcId, - rpcMethod: vscodeCommand, - data: { - error: { - ...error, - name: error.name, - message: error.message, - }, - }, - }) - } else { - this._panel.webview.postMessage({ - rpcId: message.rpcId, - rpcMethod: vscodeCommand, - data: { error }, - }) - } - } - })() - return - case 'LOAD_GCP_CREDS': - ;(async () => { - try { - const auth = new GoogleAuth({ - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) - - const client = await auth.getClient() - const projectId = await auth.getProjectId() - - const tokenResponse = await client.getAccessToken() - - this._panel.webview.postMessage({ - rpcId: message.rpcId, - rpcMethod: vscodeCommand, - data: { - ok: { - accessToken: tokenResponse.token, - projectId, - }, - }, - }) - } catch (error) { - console.error('Error loading gcp creds:', error) - if (error instanceof Error) { - this._panel.webview.postMessage({ - rpcId: message.rpcId, - rpcMethod: vscodeCommand, - data: { - error: { - ...error, - name: error.name, - message: error.message, - }, - }, - }) - } else { - this._panel.webview.postMessage({ - rpcId: message.rpcId, - rpcMethod: vscodeCommand, - data: { error }, - }) - } - } - })() - return - case 'INITIALIZED': // when the playground is initialized and listening for file changes, we should resend all project files. - // request diagnostics, which updates the runtime and triggers a new project files update. - addProject() - console.log('initialized webview') - this._panel.webview.postMessage({ rpcId: message.rpcId, rpcMethod: vscodeCommand, data: { ack: true } }) - return - } - }, - undefined, - this._disposables, - ) - } } diff --git a/typescript/vscode-ext/packages/vscode/src/plugins/language-server-client/index.ts b/typescript/vscode-ext/packages/vscode/src/plugins/language-server-client/index.ts index ec532dbbe7..6b6f7c733e 100644 --- a/typescript/vscode-ext/packages/vscode/src/plugins/language-server-client/index.ts +++ b/typescript/vscode-ext/packages/vscode/src/plugins/language-server-client/index.ts @@ -38,6 +38,8 @@ let bamlOutputChannel: OutputChannel // Variable to store the path of the currently executing CLI let currentExecutingCliPath: string | null = null +let playgroundPort: number | null = null + const isDebugMode = () => process.env.VSCODE_DEBUG_MODE === 'true' const isE2ETestOnPullRequest = () => process.env.PRISMA_USE_LOCAL_LS === 'true' @@ -98,6 +100,50 @@ export const requestBamlCLIVersion = async (): Promise => { } } +export const viewFunctionInPlayground = async (args?: { + projectId?: string + functionName?: string + implName?: string + showTests?: boolean +}): Promise => { + if (!clientReady) { + console.warn('Client not ready for viewFunctionInPlayground request') + return + } + try { + console.log('Sending changeFunction command to LSP:', args) + await client.sendRequest('workspace/executeCommand', { + command: 'baml.changeFunction', + arguments: [args], + }) + } catch (e) { + console.error('Failed to change function in playground:', e) + throw e // Re-throw to let caller handle + } +} + +export const runTestInPlayground = async (args?: { + projectId?: string + functionName?: string + testCaseName?: string + [key: string]: any +}): Promise => { + if (!clientReady) { + console.warn('Client not ready for runTestInPlayground request') + return + } + try { + console.log('Sending runTest command to LSP:', args) + await client.sendRequest('workspace/executeCommand', { + command: 'baml.runTest', + arguments: [args], + }) + } catch (e) { + console.error('Failed to run test in playground:', e) + throw e // Re-throw to let caller handle + } +} + export const getBAMLFunctions = async (): Promise< { name: string @@ -166,6 +212,10 @@ const sleep = (time: number) => { }) } +export function getPlaygroundPort(): number | null { + return playgroundPort +} + export const registerClientEventHandlers = (client: LanguageClient, context: ExtensionContext) => { client.onNotification('baml/showLanguageServerOutput', () => { // need to append line for the show to work for some reason. @@ -263,7 +313,6 @@ export const registerClientEventHandlers = (client: LanguageClient, context: Ext console.log('Received baml_settings_updated from LSP:', config) BAML_CONFIG_SINGLETON.config = config.config BAML_CONFIG_SINGLETON.cliVersion = config.cliVersion - WebviewPanelHost.currentPanel?.postMessage('baml_settings_updated', BAML_CONFIG_SINGLETON) }) const handleRuntimeUpdated = (params: { root_path: string; files: Record }) => { @@ -274,11 +323,7 @@ export const registerClientEventHandlers = (client: LanguageClient, context: Ext const currentFilePath = URI.parse(activeEditor.document.uri.toString()).fsPath const rootPathUri = URI.file(params.root_path).fsPath if (currentFilePath.startsWith(rootPathUri)) { - console.log('Forwarding runtime_updated to WebviewPanelHost') - WebviewPanelHost.currentPanel?.postMessage('add_project', { - ...params, - root_path: URI.file(params.root_path).toString(), - }) + console.log('Runtime updated for active editor') } else { console.log('runtime_updated ignored: root path does not match active editor', currentFilePath, rootPathUri) } @@ -293,6 +338,19 @@ export const registerClientEventHandlers = (client: LanguageClient, context: Ext client.onRequest('runtime_updated', handleRuntimeUpdated) client.onNotification('runtime_updated', handleRuntimeUpdated) + client.onNotification('baml/port', (params: { port: number }) => { + playgroundPort = params.port + console.log('Received playground port from LSP:', playgroundPort) + + // Update the webview panel if it exists + if (WebviewPanelHost.currentPanel) { + console.log('Updating existing webview panel with port:', playgroundPort) + WebviewPanelHost.currentPanel.updatePlaygroundPort(playgroundPort) + } else { + console.log('No webview panel exists yet, port will be used when panel is created') + } + }) + // eslint-disable-next-line @typescript-eslint/no-misused-promises client.onNotification('baml_src_generator_version', async (payload: { version: string; root_path: string }) => { try {