use deno_core::JsRuntime;
use deno_core::ModuleCodeString;
use deno_core::RuntimeOptions;
use deno_core::anyhow;
use deno_core::error::CoreError;
use futures::lock::Mutex;
use pctx_registry::PctxRegistry;
pub use pctx_type_check_runtime::{CheckResult, Diagnostic, is_relevant_error};
use pctx_type_check_runtime::{init_v8_platform, type_check};
use serde::{Deserialize, Serialize};
use std::{rc::Rc, time::SystemTime};
use thiserror::Error;
use tracing::{debug, warn};
pub mod events;
static V8_MUTEX: std::sync::LazyLock<Mutex<()>> = std::sync::LazyLock::new(|| {
init_v8_platform();
Mutex::new(())
});
pub type Result<T> = std::result::Result<T, DenoExecutorError>;
#[derive(Clone, Default)]
pub struct ExecuteOptions {
pub registry: PctxRegistry,
}
impl std::fmt::Debug for ExecuteOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExecuteOptions")
.field("registry", &self.registry.ids())
.finish()
}
}
impl ExecuteOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_registry(mut self, registry: PctxRegistry) -> Self {
self.registry = registry;
self
}
}
#[derive(Clone, Debug)]
pub struct ExecuteResult {
pub success: bool,
pub diagnostics: Vec<Diagnostic>,
pub runtime_error: Option<ExecutionError>,
pub output: Option<serde_json::Value>,
pub stdout: String,
pub stderr: String,
pub registry: PctxRegistry,
pub trace: events::ExecutionTrace,
}
#[derive(Debug, Error)]
pub enum DenoExecutorError {
#[error("Internal check error: {0}")]
InternalError(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Type check error: {0}")]
TypeCheckError(#[from] pctx_type_check_runtime::TypeCheckError),
}
pub async fn execute(code: &str, options: ExecuteOptions) -> Result<ExecuteResult> {
debug!(
code_length = code.len(),
"Code submitted for typecheck & execution"
);
let started_at = SystemTime::now();
let _guard = V8_MUTEX.lock().await;
let typecheck_started_at = SystemTime::now();
let check_result = run_type_check(code)?;
let typecheck_ended_at = SystemTime::now();
if !check_result.diagnostics.is_empty() {
let stderr = format_diagnostics(&check_result.diagnostics);
warn!(
runtime = "type_check",
diagnostic = %stderr,
diagnostic_count = check_result.diagnostics.len(),
"Type check failed with diagnostics"
);
let events = vec![events::ExecutionEvent::TypeCheck(events::TypeCheckEvent {
started_at: typecheck_started_at,
ended_at: typecheck_ended_at,
outcome: events::TypecheckOutcome::Failed {
diagnostics: check_result.diagnostics.clone(),
},
})];
return Ok(ExecuteResult {
success: false,
diagnostics: check_result.diagnostics,
runtime_error: None,
output: None,
stdout: String::new(),
stderr,
trace: events::ExecutionTrace {
code: code.to_string(),
started_at,
ended_at: SystemTime::now(),
events,
},
registry: options.registry,
});
}
debug!(runtime = "type_check", "Type check passed");
let exec_result = execute_code(code, options)
.await
.map_err(|e| DenoExecutorError::InternalError(e.to_string()))?;
let stderr = if let Some(ref err) = exec_result.error {
err.message.clone()
} else {
String::new()
};
let ended_at = SystemTime::now();
let mut events = vec![events::ExecutionEvent::TypeCheck(events::TypeCheckEvent {
started_at: typecheck_started_at,
ended_at: typecheck_ended_at,
outcome: events::TypecheckOutcome::Passed,
})];
for registry_event in exec_result.registry.trace().events() {
events.push(match registry_event {
events::RegistryEvent::McpToolCall(e) => events::ExecutionEvent::McpToolCall(e),
events::RegistryEvent::CallbackInvocation(e) => {
events::ExecutionEvent::CallbackInvocation(e)
}
});
}
events.sort_by_key(|e| e.started_at());
Ok(ExecuteResult {
success: exec_result.success,
diagnostics: Vec::new(), runtime_error: exec_result.error,
output: exec_result.output,
stdout: exec_result.stdout,
stderr: if exec_result.stderr.is_empty() {
stderr
} else {
exec_result.stderr
},
trace: events::ExecutionTrace {
code: code.to_string(),
started_at,
ended_at,
events,
},
registry: exec_result.registry,
})
}
#[tracing::instrument(fields(runtime = "type_check"))]
fn run_type_check(code: &str) -> Result<CheckResult> {
let mut check_result = type_check(code)?;
if !check_result.success && !check_result.diagnostics.is_empty() {
check_result.diagnostics = check_result
.diagnostics
.into_iter()
.filter(is_relevant_error)
.collect();
}
Ok(check_result)
}
fn format_diagnostics(diagnostics: &[Diagnostic]) -> String {
diagnostics
.iter()
.map(|d| {
let mut parts = Vec::new();
if let Some(line) = d.line {
if let Some(col) = d.column {
parts.push(format!("Line {line}, Column {col}"));
} else {
parts.push(format!("Line {line}"));
}
}
if let Some(code) = d.code {
parts.push(format!("TS{code}"));
}
let cleaned_message = d
.message
.replace("file:///check.ts:", "")
.trim()
.to_string();
if parts.is_empty() {
cleaned_message
} else {
format!("{}: {}", parts.join(", "), cleaned_message)
}
})
.collect::<Vec<_>>()
.join("\n")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionError {
pub message: String,
pub stack: Option<String>,
}
#[derive(Debug, Clone)]
struct InternalExecuteResult {
pub success: bool,
pub output: Option<serde_json::Value>,
pub error: Option<ExecutionError>,
pub stdout: String,
pub stderr: String,
pub registry: PctxRegistry,
}
#[tracing::instrument(fields(runtime = "execution"))]
async fn execute_code(
code: &str,
options: ExecuteOptions,
) -> anyhow::Result<InternalExecuteResult> {
debug!("Starting code execution");
let js_code = match pctx_deno_transpiler::transpile(code, None) {
Ok(js) => {
debug!(
runtime = "execution",
transpiled_code_length = js.len(),
"Code transpiled successfully"
);
js
}
Err(e) => {
warn!(runtime = "execution", error = %e, "Transpilation failed");
return Ok(InternalExecuteResult {
success: false,
output: None,
error: Some(ExecutionError {
message: format!("Transpilation failed: {e}"),
stack: None,
}),
stdout: String::new(),
stderr: String::new(),
registry: options.registry,
});
}
};
let extensions = vec![pctx_code_execution_runtime::pctx_runtime_snapshot::init(
options.registry.clone(),
)];
let mut js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
startup_snapshot: Some(pctx_code_execution_runtime::RUNTIME_SNAPSHOT),
extensions,
..Default::default()
});
let main_module = deno_core::resolve_url("file:///execute.js")?;
debug!(main_module =? main_module, "Loading module into runtime");
let mod_id = match js_runtime
.load_side_es_module_from_code(&main_module, ModuleCodeString::from(js_code))
.await
{
Ok(id) => {
debug!(module_id = id, "Module loaded successfully");
id
}
Err(e) => {
warn!(error = %e, "Failed to load module");
return Ok(InternalExecuteResult {
success: false,
output: None,
error: Some(ExecutionError {
message: e.to_string(),
stack: None,
}),
stdout: String::new(),
stderr: String::new(),
registry: options.registry,
});
}
};
debug!("Evaluating module");
let eval_future = js_runtime.mod_evaluate(mod_id);
debug!("Running event loop");
let event_loop_future = js_runtime.run_event_loop(deno_core::PollEventLoopOptions {
wait_for_inspector: false,
pump_v8_message_loop: true,
});
let (eval_result, event_loop_result) = futures::join!(eval_future, event_loop_future);
debug!("Eval and event loop futures resolved");
process_execution_results(
&mut js_runtime,
mod_id,
eval_result.err(),
event_loop_result.err(),
options.registry,
)
}
#[tracing::instrument(skip_all)]
fn process_execution_results(
js_runtime: &mut JsRuntime,
mod_id: usize,
eval_err: Option<CoreError>,
event_loop_err: Option<CoreError>,
registry: PctxRegistry,
) -> anyhow::Result<InternalExecuteResult> {
let (success, error) = match (eval_err, event_loop_err) {
(None, None) => {
debug!("Code executed successfully");
(true, None)
}
(Some(e), _) | (_, Some(e)) => {
warn!( error = %e, "Code execution failed");
(
false,
Some(ExecutionError {
message: e.to_string(),
stack: None,
}),
)
}
};
let capture_script = r"
({
stdout: globalThis.__stdout || [],
stderr: globalThis.__stderr || []
})
";
let console_global = js_runtime
.execute_script("<capture_output>", capture_script)
.ok();
let module_namespace = if success {
js_runtime.get_module_namespace(mod_id).ok()
} else {
None
};
deno_core::scope!(scope, js_runtime);
let console_output = console_global.and_then(|global| {
let local = deno_core::v8::Local::new(scope, global);
deno_core::serde_v8::from_v8::<serde_json::Value>(scope, local).ok()
});
let stdout_str = console_output
.as_ref()
.and_then(|v| v["stdout"].as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join("\n")
})
.unwrap_or_default();
let stderr_str = console_output
.as_ref()
.and_then(|v| v["stderr"].as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join("\n")
})
.unwrap_or_default();
let output: Option<serde_json::Value> = module_namespace.and_then(|module_namespace| {
let namespace = deno_core::v8::Local::new(scope, module_namespace);
let default_key = deno_core::v8::String::new(scope, "default")?;
namespace
.get(scope, default_key.into())
.and_then(|default_value| {
if default_value.is_undefined() {
return None;
}
if default_value.is_promise() {
let promise = default_value.cast::<deno_core::v8::Promise>();
if promise.state() == deno_core::v8::PromiseState::Fulfilled {
let result = promise.result(scope);
return deno_core::serde_v8::from_v8(scope, result).ok();
}
return None;
}
deno_core::serde_v8::from_v8(scope, default_value).ok()
})
});
Ok(InternalExecuteResult {
success,
output,
error,
stdout: stdout_str,
stderr: stderr_str,
registry,
})
}
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}