use anyhow::{Context, Result};
use serde::Deserialize;
use tracing::debug;
#[derive(Debug, Clone, Deserialize)]
pub struct RenderResult {
pub body: String,
#[serde(default)]
pub head: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RscRenderResult {
pub body: String,
#[serde(default)]
pub head: String,
pub flight: String,
}
pub struct SsrIsolate {
pub(crate) isolate: v8::OwnedIsolate,
pub(crate) context: v8::Global<v8::Context>,
pub(crate) render_fn: v8::Global<v8::Function>,
pub(crate) gssp_fn: v8::Global<v8::Function>,
pub(crate) gsp_fn: v8::Global<v8::Function>,
pub(crate) api_handler_fn: Option<v8::Global<v8::Function>>,
pub(crate) document_fn: Option<v8::Global<v8::Function>>,
pub(crate) middleware_fn: Option<v8::Global<v8::Function>>,
pub(crate) rsc_flight_fn: Option<v8::Global<v8::Function>>,
pub(crate) rsc_to_html_fn: Option<v8::Global<v8::Function>>,
pub(crate) mcp_call_fn: Option<v8::Global<v8::Function>>,
pub(crate) mcp_list_fn: Option<v8::Global<v8::Function>>,
pub(crate) server_action_fn: Option<v8::Global<v8::Function>>,
pub(crate) server_action_encoded_fn: Option<v8::Global<v8::Function>>,
pub(crate) form_action_fn: Option<v8::Global<v8::Function>>,
pub(crate) app_route_handler_fn: Option<v8::Global<v8::Function>>,
pub(crate) gsp_paths_fn: Option<v8::Global<v8::Function>>,
pub(crate) last_bundle: std::sync::Arc<String>,
}
impl SsrIsolate {
pub fn new(server_bundle_js: &str, project_root: Option<&str>) -> Result<Self> {
let mut isolate = v8::Isolate::new(v8::CreateParams::default());
isolate.set_microtasks_policy(v8::MicrotasksPolicy::Explicit);
isolate.set_promise_reject_callback(promise_reject_callback);
let (
context,
render_fn,
gssp_fn,
gsp_fn,
api_handler_fn,
document_fn,
middleware_fn,
rsc_flight_fn,
rsc_to_html_fn,
mcp_call_fn,
mcp_list_fn,
server_action_fn,
server_action_encoded_fn,
form_action_fn,
app_route_handler_fn,
gsp_paths_fn,
) = {
v8::scope!(scope, &mut isolate);
let context = v8::Context::new(scope, Default::default());
let scope = &mut v8::ContextScope::new(scope, context);
{
let global = context.global(scope);
let console = v8::Object::new(scope);
let t = v8::FunctionTemplate::new(scope, console_log);
let f = t
.get_function(scope)
.ok_or_else(|| anyhow::anyhow!("Failed to create console.log"))?;
let k = v8::String::new(scope, "log")
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
console.set(scope, k.into(), f.into());
let t = v8::FunctionTemplate::new(scope, console_warn);
let f = t
.get_function(scope)
.ok_or_else(|| anyhow::anyhow!("Failed to create console.warn"))?;
let k = v8::String::new(scope, "warn")
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
console.set(scope, k.into(), f.into());
let t = v8::FunctionTemplate::new(scope, console_error);
let f = t
.get_function(scope)
.ok_or_else(|| anyhow::anyhow!("Failed to create console.error"))?;
let k = v8::String::new(scope, "error")
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
console.set(scope, k.into(), f.into());
let t = v8::FunctionTemplate::new(scope, console_log);
let f = t
.get_function(scope)
.ok_or_else(|| anyhow::anyhow!("Failed to create console.info"))?;
let k = v8::String::new(scope, "info")
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
console.set(scope, k.into(), f.into());
let k = v8::String::new(scope, "console")
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
global.set(scope, k.into(), console.into());
let k = v8::String::new(scope, "globalThis")
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
global.set(scope, k.into(), global.into());
let t = v8::FunctionTemplate::new(scope, crate::fetch::fetch_callback);
let f = t
.get_function(scope)
.ok_or_else(|| anyhow::anyhow!("Failed to create fetch"))?;
let k = v8::String::new(scope, "fetch")
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
global.set(scope, k.into(), f.into());
let t = v8::FunctionTemplate::new(scope, queue_microtask_callback);
let f = t
.get_function(scope)
.ok_or_else(|| anyhow::anyhow!("Failed to create queueMicrotask"))?;
let k = v8::String::new(scope, "queueMicrotask")
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
global.set(scope, k.into(), f.into());
crate::fs::register_fs_callbacks(scope, global)?;
crate::tcp::register_tcp_callbacks(scope, global)?;
if let Some(root) = project_root {
let k = v8::String::new(scope, "__rex_project_root")
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let v = v8::String::new(scope, root)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
global.set(scope, k.into(), v.into());
}
}
let process_env_script = build_process_env_script();
v8_eval!(scope, &process_env_script, "<process-env>")
.context("Failed to inject process.env")?;
v8_eval!(scope, server_bundle_js, "server-bundle.js")
.context("Failed to evaluate server bundle")?;
let ctx = scope.get_current_context();
let global = ctx.global(scope);
let render_fn = v8_get_global_fn!(scope, global, "__rex_render_page")?;
let gssp_fn = v8_get_global_fn!(scope, global, "__rex_get_server_side_props")?;
let gsp_fn = v8_get_global_fn!(scope, global, "__rex_get_static_props")?;
let api_handler_fn = v8_get_optional_fn!(scope, global, "__rex_call_api_handler");
let document_fn = v8_get_optional_fn!(scope, global, "__rex_render_document");
let middleware_fn = v8_get_optional_fn!(scope, global, "__rex_run_middleware");
let rsc_flight_fn = v8_get_optional_fn!(scope, global, "__rex_render_flight");
let rsc_to_html_fn = v8_get_optional_fn!(scope, global, "__rex_render_rsc_to_html");
let mcp_call_fn = v8_get_optional_fn!(scope, global, "__rex_call_mcp_tool");
let mcp_list_fn = v8_get_optional_fn!(scope, global, "__rex_list_mcp_tools");
let server_action_fn = v8_get_optional_fn!(scope, global, "__rex_call_server_action");
let server_action_encoded_fn =
v8_get_optional_fn!(scope, global, "__rex_call_server_action_encoded");
let form_action_fn = v8_get_optional_fn!(scope, global, "__rex_call_form_action");
let app_route_handler_fn =
v8_get_optional_fn!(scope, global, "__rex_call_app_route_handler");
let gsp_paths_fn = v8_get_optional_fn!(scope, global, "__rex_get_static_paths");
(
v8::Global::new(scope, context),
v8::Global::new(scope, render_fn),
v8::Global::new(scope, gssp_fn),
v8::Global::new(scope, gsp_fn),
api_handler_fn,
document_fn,
middleware_fn,
rsc_flight_fn,
rsc_to_html_fn,
mcp_call_fn,
mcp_list_fn,
server_action_fn,
server_action_encoded_fn,
form_action_fn,
app_route_handler_fn,
gsp_paths_fn,
)
};
Ok(Self {
isolate,
context,
render_fn,
gssp_fn,
gsp_fn,
api_handler_fn,
document_fn,
middleware_fn,
rsc_flight_fn,
rsc_to_html_fn,
mcp_call_fn,
mcp_list_fn,
server_action_fn,
server_action_encoded_fn,
form_action_fn,
app_route_handler_fn,
gsp_paths_fn,
last_bundle: std::sync::Arc::new(server_bundle_js.to_string()),
})
}
pub fn render_page(&mut self, route_key: &str, props_json: &str) -> Result<RenderResult> {
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let func = v8::Local::new(scope, &self.render_fn);
let undef = v8::undefined(scope);
let arg0 = v8::String::new(scope, route_key)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let arg1 = v8::String::new(scope, props_json)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let result = v8_call!(scope, func, undef.into(), &[arg0.into(), arg1.into()])
.map_err(|e| anyhow::anyhow!("SSR render error: {e}"))?;
let json_str = result.to_rust_string_lossy(scope);
serde_json::from_str(&json_str).context("Failed to parse render result JSON")
}
pub fn get_server_side_props(&mut self, route_key: &str, context_json: &str) -> Result<String> {
let result_str = {
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let func = v8::Local::new(scope, &self.gssp_fn);
let undef = v8::undefined(scope);
let arg0 = v8::String::new(scope, route_key)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let arg1 = v8::String::new(scope, context_json)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let result = v8_call!(scope, func, undef.into(), &[arg0.into(), arg1.into()])
.map_err(|e| anyhow::anyhow!("GSSP error: {e}"))?;
result.to_rust_string_lossy(scope)
};
if result_str == "__REX_ASYNC__" {
crate::fetch::run_fetch_loop(&mut self.isolate, &self.context);
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let resolve_result =
v8_eval!(scope, "globalThis.__rex_resolve_gssp()", "<gssp-resolve>")
.map_err(|e| anyhow::anyhow!("GSSP error: {e}"))?;
Ok(resolve_result.to_rust_string_lossy(scope))
} else {
Ok(result_str)
}
}
pub fn get_static_props(&mut self, route_key: &str, context_json: &str) -> Result<String> {
let result_str = {
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let func = v8::Local::new(scope, &self.gsp_fn);
let undef = v8::undefined(scope);
let arg0 = v8::String::new(scope, route_key)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let arg1 = v8::String::new(scope, context_json)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let result = v8_call!(scope, func, undef.into(), &[arg0.into(), arg1.into()])
.map_err(|e| anyhow::anyhow!("GSP error: {e}"))?;
result.to_rust_string_lossy(scope)
};
if result_str == "__REX_GSP_ASYNC__" {
crate::fetch::run_fetch_loop(&mut self.isolate, &self.context);
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let resolve_result = v8_eval!(scope, "globalThis.__rex_resolve_gsp()", "<gsp-resolve>")
.map_err(|e| anyhow::anyhow!("GSP error: {e}"))?;
Ok(resolve_result.to_rust_string_lossy(scope))
} else {
Ok(result_str)
}
}
pub fn call_api_handler(&mut self, route_key: &str, req_json: &str) -> Result<String> {
let api_fn = self
.api_handler_fn
.as_ref()
.ok_or_else(|| anyhow::anyhow!("API handlers not loaded"))?;
let result_str = {
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let func = v8::Local::new(scope, api_fn);
let undef = v8::undefined(scope);
let arg0 = v8::String::new(scope, route_key)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let arg1 = v8::String::new(scope, req_json)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let result = v8_call!(scope, func, undef.into(), &[arg0.into(), arg1.into()])
.map_err(|e| anyhow::anyhow!("API handler error: {e}"))?;
result.to_rust_string_lossy(scope)
};
if result_str == "__REX_API_ASYNC__" {
crate::fetch::run_fetch_loop(&mut self.isolate, &self.context);
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let resolve_result = v8_eval!(scope, "globalThis.__rex_resolve_api()", "<api-resolve>")
.map_err(|e| anyhow::anyhow!("API handler error: {e}"))?;
Ok(resolve_result.to_rust_string_lossy(scope))
} else {
Ok(result_str)
}
}
pub fn call_app_route_handler(
&mut self,
route_pattern: &str,
req_json: &str,
) -> Result<String> {
let handler_fn = self
.app_route_handler_fn
.as_ref()
.ok_or_else(|| anyhow::anyhow!("App route handlers not loaded"))?;
let result_str = {
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let func = v8::Local::new(scope, handler_fn);
let undef = v8::undefined(scope);
let arg0 = v8::String::new(scope, route_pattern)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let arg1 = v8::String::new(scope, req_json)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let result = v8_call!(scope, func, undef.into(), &[arg0.into(), arg1.into()])
.map_err(|e| anyhow::anyhow!("App route handler error: {e}"))?;
result.to_rust_string_lossy(scope)
};
if result_str == "__REX_APP_ROUTE_ASYNC__" {
crate::fetch::run_fetch_loop(&mut self.isolate, &self.context);
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let resolve_result = v8_eval!(
scope,
"globalThis.__rex_resolve_app_route()",
"<app-route-resolve>"
)
.map_err(|e| anyhow::anyhow!("App route handler error: {e}"))?;
Ok(resolve_result.to_rust_string_lossy(scope))
} else {
Ok(result_str)
}
}
pub fn render_document(&mut self) -> Result<Option<String>> {
let doc_fn = match &self.document_fn {
Some(f) => f,
None => return Ok(None),
};
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let func = v8::Local::new(scope, doc_fn);
let undef = v8::undefined(scope);
let result = v8_call!(scope, func, undef.into(), &[])
.map_err(|e| anyhow::anyhow!("Document render error: {e}"))?;
Ok(Some(result.to_rust_string_lossy(scope)))
}
pub fn run_middleware(&mut self, req_json: &str) -> Result<Option<String>> {
let mw_fn = match &self.middleware_fn {
Some(f) => f,
None => return Ok(None),
};
let result_str = {
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let func = v8::Local::new(scope, mw_fn);
let undef = v8::undefined(scope);
let arg0 = v8::String::new(scope, req_json)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let result = v8_call!(scope, func, undef.into(), &[arg0.into()])
.map_err(|e| anyhow::anyhow!("Middleware error: {e}"))?;
result.to_rust_string_lossy(scope)
};
if result_str == "__REX_MW_ASYNC__" {
crate::fetch::run_fetch_loop(&mut self.isolate, &self.context);
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let resolve_result = v8_eval!(
scope,
"globalThis.__rex_resolve_middleware()",
"<mw-resolve>"
)
.map_err(|e| anyhow::anyhow!("Middleware error: {e}"))?;
Ok(Some(resolve_result.to_rust_string_lossy(scope)))
} else {
Ok(Some(result_str))
}
}
pub fn reload(&mut self, server_bundle_js: &str) -> Result<()> {
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
v8_eval!(scope, "globalThis.__rex_pages = {};", "<reload>")?;
if let Err(e) = v8_eval!(scope, server_bundle_js, "server-bundle.js") {
tracing::warn!("New bundle failed, restoring previous: {e}");
v8_eval!(scope, "globalThis.__rex_pages = {};", "<restore>")?;
v8_eval!(scope, &self.last_bundle, "server-bundle.js")
.context("Failed to restore previous server bundle")?;
return Err(e.context("Failed to evaluate updated server bundle"));
}
let ctx = scope.get_current_context();
let global = ctx.global(scope);
let render_fn = v8_get_global_fn!(scope, global, "__rex_render_page")?;
let gssp_fn = v8_get_global_fn!(scope, global, "__rex_get_server_side_props")?;
let gsp_fn = v8_get_global_fn!(scope, global, "__rex_get_static_props")?;
self.render_fn = v8::Global::new(scope, render_fn);
self.gssp_fn = v8::Global::new(scope, gssp_fn);
self.gsp_fn = v8::Global::new(scope, gsp_fn);
self.api_handler_fn = v8_get_optional_fn!(scope, global, "__rex_call_api_handler");
self.document_fn = v8_get_optional_fn!(scope, global, "__rex_render_document");
self.middleware_fn = v8_get_optional_fn!(scope, global, "__rex_run_middleware");
self.rsc_flight_fn = v8_get_optional_fn!(scope, global, "__rex_render_flight");
self.rsc_to_html_fn = v8_get_optional_fn!(scope, global, "__rex_render_rsc_to_html");
self.mcp_call_fn = v8_get_optional_fn!(scope, global, "__rex_call_mcp_tool");
self.mcp_list_fn = v8_get_optional_fn!(scope, global, "__rex_list_mcp_tools");
self.server_action_fn = v8_get_optional_fn!(scope, global, "__rex_call_server_action");
self.server_action_encoded_fn =
v8_get_optional_fn!(scope, global, "__rex_call_server_action_encoded");
self.form_action_fn = v8_get_optional_fn!(scope, global, "__rex_call_form_action");
self.app_route_handler_fn =
v8_get_optional_fn!(scope, global, "__rex_call_app_route_handler");
self.gsp_paths_fn = v8_get_optional_fn!(scope, global, "__rex_get_static_paths");
self.last_bundle = std::sync::Arc::new(server_bundle_js.to_string());
debug!("SSR isolate reloaded");
Ok(())
}
pub fn get_static_paths(&mut self, route_key: &str) -> Result<String> {
let paths_fn = self
.gsp_paths_fn
.as_ref()
.ok_or_else(|| anyhow::anyhow!("getStaticPaths runtime not loaded"))?;
let result_str = {
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let func = v8::Local::new(scope, paths_fn);
let undef = v8::undefined(scope);
let arg0 = v8::String::new(scope, route_key)
.ok_or_else(|| anyhow::anyhow!("V8 string alloc failed"))?;
let result = v8_call!(scope, func, undef.into(), &[arg0.into()])
.map_err(|e| anyhow::anyhow!("getStaticPaths error: {e}"))?;
result.to_rust_string_lossy(scope)
};
if result_str == "__REX_GSP_PATHS_ASYNC__" {
crate::fetch::run_fetch_loop(&mut self.isolate, &self.context);
v8::scope_with_context!(scope, &mut self.isolate, &self.context);
let resolve_result = v8_eval!(
scope,
"globalThis.__rex_resolve_static_paths()",
"<gsp-paths-resolve>"
)
.map_err(|e| anyhow::anyhow!("getStaticPaths error: {e}"))?;
Ok(resolve_result.to_rust_string_lossy(scope))
} else {
Ok(result_str)
}
}
}
fn build_process_env_script() -> String {
use std::fmt::Write;
let mut pairs = String::new();
for (key, value) in std::env::vars() {
let key_json = serde_json::to_string(&key).unwrap_or_default();
let val_json = serde_json::to_string(&value).unwrap_or_default();
if !pairs.is_empty() {
pairs.push(',');
}
let _ = write!(pairs, "{key_json}:{val_json}");
}
format!(
"if(!globalThis.process){{globalThis.process={{}}}};globalThis.process.env={{{pairs}}};"
)
}
fn format_args(scope: &mut v8::PinScope, args: &v8::FunctionCallbackArguments) -> String {
let mut parts = Vec::new();
for i in 0..args.length() {
let arg = args.get(i);
parts.push(arg.to_rust_string_lossy(scope));
}
parts.join(" ")
}
#[allow(unsafe_code)]
unsafe extern "C" fn promise_reject_callback(msg: v8::PromiseRejectMessage) {
let event = msg.get_event();
match event {
v8::PromiseRejectEvent::PromiseRejectWithNoHandler => {
tracing::warn!("Unhandled promise rejection (no handler attached)");
}
v8::PromiseRejectEvent::PromiseHandlerAddedAfterReject => {
}
_ => {
tracing::warn!("Promise reject event: {:?}", event);
}
}
}
fn console_log(scope: &mut v8::PinScope, args: v8::FunctionCallbackArguments, _: v8::ReturnValue) {
tracing::info!(target: "v8::console", "{}", format_args(scope, &args));
}
fn console_warn(scope: &mut v8::PinScope, args: v8::FunctionCallbackArguments, _: v8::ReturnValue) {
tracing::warn!(target: "v8::console", "{}", format_args(scope, &args));
}
fn console_error(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
_: v8::ReturnValue,
) {
tracing::error!(target: "v8::console", "{}", format_args(scope, &args));
}
fn queue_microtask_callback(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
_ret: v8::ReturnValue,
) {
if args.length() < 1 || !args.get(0).is_function() {
return;
}
let func = v8::Local::<v8::Function>::try_from(args.get(0)).expect("queueMicrotask arg");
scope.enqueue_microtask(func);
}