use std::collections::HashMap;
#[derive(Debug)]
pub struct Ssr<'s, 'i> {
isolate: *mut v8::OwnedIsolate,
handle_scope: *mut v8::HandleScope<'s, ()>,
fn_map: HashMap<String, v8::Local<'s, v8::Function>>,
scope: *mut v8::ContextScope<'i, v8::HandleScope<'s>>,
}
impl Drop for Ssr<'_, '_> {
fn drop(&mut self) {
self.fn_map.clear();
unsafe {
let _ = Box::from_raw(self.scope);
let _ = Box::from_raw(self.handle_scope);
let _ = Box::from_raw(self.isolate);
};
}
}
impl<'s, 'i> Ssr<'s, 'i>
where
's: 'i,
{
pub fn create_platform() {
let platform = v8::new_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
}
pub fn from(source: String, entry_point: &str) -> Result<Self, &'static str> {
let isolate = Box::into_raw(Box::new(v8::Isolate::new(v8::CreateParams::default())));
let handle_scope = unsafe { Box::into_raw(Box::new(v8::HandleScope::new(&mut *isolate))) };
let context = unsafe { v8::Context::new(&mut *handle_scope) };
let scope_ptr =
unsafe { Box::into_raw(Box::new(v8::ContextScope::new(&mut *handle_scope, context))) };
let scope = unsafe { &mut *scope_ptr };
let code = match v8::String::new(scope, &format!("{source};{entry_point}")) {
Some(val) => val,
None => return Err("Invalid JS: Strings are needed"),
};
let script = match v8::Script::compile(scope, code, None) {
Some(val) => val,
None => return Err("Invalid JS: There aren't runnable scripts"),
};
let exports = match script.run(scope) {
Some(val) => val,
None => return Err("Invalid JS: Execute your script with d8 to debug"),
};
let object = match exports.to_object(scope) {
Some(val) => val,
None => {
return Err(
"Invalid JS: The script does not return any object after being executed",
)
}
};
let mut fn_map: HashMap<String, v8::Local<v8::Function>> = HashMap::new();
if let Some(props) = object.get_own_property_names(scope, Default::default()) {
fn_map = match Some(props)
.iter()
.enumerate()
.map(
|(i, &p)| -> Result<(String, v8::Local<v8::Function>), &'static str> {
let name = match p.get_index(scope, i as u32) {
Some(val) => val,
None => return Err("Failed to get function name"),
};
let mut scope = v8::EscapableHandleScope::new(scope);
let func = match object.get(&mut scope, name) {
Some(val) => val,
None => return Err("Failed to get function from obj"),
};
let func = unsafe { v8::Local::<v8::Function>::cast(func) };
let fn_name = match name.to_string(&mut scope) {
Some(val) => val.to_rust_string_lossy(&mut scope),
None => return Err("Failed to find function name"),
};
Ok((fn_name, scope.escape(func)))
},
)
.collect()
{
Ok(val) => val,
Err(err) => return Err(err),
}
}
Ok(Ssr {
isolate,
handle_scope,
fn_map,
scope: scope_ptr,
})
}
pub fn render_to_string(&mut self, params: Option<&str>) -> Result<String, &'static str> {
let scope = unsafe { &mut *self.scope };
let params: v8::Local<v8::Value> = match v8::String::new(scope, params.unwrap_or("")) {
Some(s) => s.into(),
None => v8::undefined(scope).into(),
};
let undef = v8::undefined(scope).into();
let mut rendered = String::new();
for key in self.fn_map.keys() {
let result = match self.fn_map[key].call(scope, undef, &[params]) {
Some(val) => val,
None => return Err("Failed to call function"),
};
let result = match result.to_string(scope) {
Some(val) => val,
None => return Err("Failed to parse the result to string"),
};
rendered = format!("{}{}", rendered, result.to_rust_string_lossy(scope));
}
Ok(rendered)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Once;
static INIT: Once = Once::new();
pub fn init_test() {
INIT.call_once(|| {
Ssr::create_platform();
})
}
#[test]
fn wrong_entry_point() {
init_test();
let source = r##"var entryPoint = {x: () => "<html></html>"};"##;
let res = Ssr::from(source.to_owned(), "IncorrectEntryPoint");
assert_eq!(
res.unwrap_err(),
"Invalid JS: Execute your script with d8 to debug"
);
}
#[test]
fn empty_code() {
init_test();
let source = r##""##;
let res = Ssr::from(source.to_owned(), "SSR");
assert_eq!(
res.unwrap_err(),
"Invalid JS: Execute your script with d8 to debug"
);
}
#[test]
fn executes_iife_source() {
init_test();
let source = r##"(() => ({x: () => 'rendered HTML'}))()"##;
let mut js = Ssr::from(source.to_owned(), "").unwrap();
assert_eq!(js.render_to_string(None).unwrap(), "rendered HTML");
}
#[test]
fn pass_param_to_function() {
init_test();
let props = r#"{"Hello world"}"#;
let accept_params_source =
r##"var SSR = {x: (params) => "These are our parameters: " + params};"##.to_string();
let mut js = Ssr::from(accept_params_source, "SSR").unwrap();
println!("Before render_to_string");
let result = js.render_to_string(Some(&props)).unwrap();
assert_eq!(result, "These are our parameters: {\"Hello world\"}");
let no_params_source = r##"var SSR = {x: () => "I don't accept params"};"##.to_string();
let mut js2 = Ssr::from(no_params_source, "SSR").unwrap();
let result2 = js2.render_to_string(Some(&props)).unwrap();
assert_eq!(result2, "I don't accept params");
let result3 = js.render_to_string(None).unwrap();
assert_eq!(result3, "These are our parameters: ");
}
#[test]
fn render_simple_html() {
init_test();
let source = r##"var SSR = {x: () => "<html></html>"};"##.to_string();
let mut js = Ssr::from(source, "SSR").unwrap();
let html = js.render_to_string(None).unwrap();
assert_eq!(html, "<html></html>");
let source2 = r##"var SSR = {x: () => "<html></html>"}"##.to_string();
let mut js2 = Ssr::from(source2, "SSR").unwrap();
let html2 = js2.render_to_string(None).unwrap();
assert_eq!(html2, "<html></html>");
}
#[test]
fn render_from_struct_instance() {
init_test();
let mut js = Ssr::from(
r##"var SSR = {x: () => "<html></html>"};"##.to_string(),
"SSR",
)
.unwrap();
assert_eq!(js.render_to_string(None).unwrap(), "<html></html>");
assert_eq!(
js.render_to_string(Some(r#"{"Hello world"}"#)).unwrap(),
"<html></html>"
);
let mut js2 = Ssr::from(
r##"var SSR = {x: () => "I don't accept params"};"##.to_string(),
"SSR",
)
.unwrap();
assert_eq!(js2.render_to_string(None).unwrap(), "I don't accept params");
}
}