diff --git a/README.md b/README.md index 7d7b403..f45b393 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The project aims to enable server side rendering on rust servers in the simplest It use an embedded version of the v8 javascript engine (rusty_v8) to parse and evaluate a built bundle file and return a string with the rendered html. -Currently it works with Webpack bundler v5.65.0; check it out here a full project who use this crate. +Currently it works with Webpack bundler v5.65.0. ## Getting started @@ -15,7 +15,7 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -ssr_rs = "0.2.3" +ssr_rs = "0.3.0" ``` ## Example @@ -32,9 +32,9 @@ fn main() { let source = read_to_string("./path/to/build.js").unwrap(); - let mut js = Ssr::new(&source, "entryPoint"); + let mut js = Ssr::new(&source, "entryPoint").unwrap(); - let html = js.render_to_string(None); + let html = js.render_to_string(None).unwrap(); assert_eq!(html, "...".to_string()); } @@ -60,14 +60,65 @@ fn main() { let source = read_to_string("./path/to/build.js").unwrap(); - let mut js = Ssr::new(&source, "entryPoint"); + let mut js = Ssr::new(&source, "entryPoint").unwrap(); - let html = js.render_to_string(Some(&props)); + let html = js.render_to_string(Some(&props)).unwrap(); assert_eq!(html, "...".to_string()); } ``` +## Example with actix-web + +> Examples with different web frameworks are available in the examples folder. + +Even though the V8 engine allows accessing the same `isolate` from different threads that is forbidden by this crate for two reasons: + +1. rusty_v8 library have not implemented yet the V8 Locker API. Accessing Ssr struct from a different thread will make the V8 engine to panic. +2. Rendering HTML does not need shared state across threads. + +For this reason parallel computation is a better choice. Following actix-web setup: + +```rust +use actix_web::{get, http::StatusCode, App, HttpResponse, HttpServer}; +use std::cell::RefCell; +use std::fs::read_to_string; + +use ssr_rs::Ssr; + +thread_local! { + static SSR: RefCell> = RefCell::new( + Ssr::from( + read_to_string("./client/dist/ssr/index.js").unwrap(), + "SSR" + ).unwrap() + ) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + + Ssr::create_platform(); + + HttpServer::new(|| { + App::new() + .service(index) + }) + .bind("127.0.0.1:8080")? + .run() + .await +} + +#[get("/")] +async fn index() -> HttpResponse { + let result = SSR.with(|ssr| ssr.borrow_mut().render_to_string(None).unwrap()); + + HttpResponse::build(StatusCode::OK) + .content_type("text/html; charset=utf-8") + .body(result) +} +``` + ## Contributing Any helps or suggestions will be appreciated. diff --git a/examples/actix.rs b/examples/actix.rs index 7155fca..e83a31b 100644 --- a/examples/actix.rs +++ b/examples/actix.rs @@ -11,7 +11,7 @@ thread_local! { Ssr::from( read_to_string("./client/dist/ssr/index.js").unwrap(), "SSR" - ) + ).unwrap() ) } @@ -37,9 +37,9 @@ async fn main() -> std::io::Result<()> { #[get("/")] async fn index() -> HttpResponse { let start = Instant::now(); - let result = SSR.with(|ssr| ssr.borrow_mut().render_to_string(None)); + let result = SSR.with(|ssr| ssr.borrow_mut().render_to_string(None).unwrap()); println!("Elapsed: {:?}", start.elapsed()); - // This is a benchmark example. Please refer to examples/shared_ssr.rs for a better solution. + HttpResponse::build(StatusCode::OK) .content_type("text/html; charset=utf-8") .body(result) diff --git a/examples/actix_with_initial_props.rs b/examples/actix_with_initial_props.rs index 5cc6c4a..4387229 100644 --- a/examples/actix_with_initial_props.rs +++ b/examples/actix_with_initial_props.rs @@ -11,7 +11,7 @@ thread_local! { Ssr::from( read_to_string("./client/dist/ssr/index.js").unwrap(), "SSR" - ) + ).unwrap() ) } @@ -42,5 +42,5 @@ async fn index() -> HttpResponse { HttpResponse::build(StatusCode::OK) .content_type("text/html; charset=utf-8") - .body(SSR.with(|ssr| ssr.borrow_mut().render_to_string(Some(mock_props)))) + .body(SSR.with(|ssr| ssr.borrow_mut().render_to_string(Some(mock_props)).unwrap())) } diff --git a/examples/axum.rs b/examples/axum.rs index aebb4ae..fe115f6 100644 --- a/examples/axum.rs +++ b/examples/axum.rs @@ -9,7 +9,7 @@ thread_local! { Ssr::from( read_to_string("./client/dist/ssr/index.js").unwrap(), "SSR" - ) + ).unwrap() ) } @@ -29,5 +29,5 @@ async fn root() -> Html { let start = Instant::now(); let result = SSR.with(|ssr| ssr.borrow_mut().render_to_string(None)); println!("Elapsed: {:?}", start.elapsed()); - Html(result) + Html(result.unwrap()) } diff --git a/examples/multi-thread.rs b/examples/multi-thread.rs index 4b38d48..71f6d3e 100644 --- a/examples/multi-thread.rs +++ b/examples/multi-thread.rs @@ -9,7 +9,7 @@ thread_local! { Ssr::from( read_to_string("./client/dist/ssr/index.js").unwrap(), "SSR" - ) + ).unwrap() ) } @@ -23,7 +23,7 @@ fn main() { let start = Instant::now(); println!( "result: {}", - SSR.with(|ssr| ssr.borrow_mut().render_to_string(None)) + SSR.with(|ssr| ssr.borrow_mut().render_to_string(None).unwrap()) ); println!( "Thread #{i} finished! - Elapsed time: {:?}", diff --git a/examples/rocket.rs b/examples/rocket.rs index 45bc4ca..7834e88 100644 --- a/examples/rocket.rs +++ b/examples/rocket.rs @@ -12,7 +12,7 @@ thread_local! { Ssr::from( read_to_string("./client/dist/ssr/index.js").unwrap(), "SSR" - ) + ).unwrap() ) } @@ -21,7 +21,7 @@ fn index() -> content::RawHtml { let start = Instant::now(); let result = SSR.with(|ssr| ssr.borrow_mut().render_to_string(None)); println!("Elapsed: {:?}", start.elapsed()); - content::RawHtml(result) + content::RawHtml(result.unwrap()) } #[launch] diff --git a/examples/run.rs b/examples/run.rs index 719259a..57d2daa 100644 --- a/examples/run.rs +++ b/examples/run.rs @@ -8,9 +8,9 @@ fn main() { Ssr::create_platform(); // This takes roughly 40ms - let mut ssr = Ssr::from(source, "SSR"); + let mut ssr = Ssr::from(source, "SSR").unwrap(); // This takes roughly 0.5ms - println!("{}", ssr.render_to_string(None)); - println!("{}", ssr.render_to_string(None)); + println!("{}", ssr.render_to_string(None).unwrap()); + println!("{}", ssr.render_to_string(None).unwrap()); } diff --git a/examples/tide.rs b/examples/tide.rs index 0ccc0f3..23c2635 100644 --- a/examples/tide.rs +++ b/examples/tide.rs @@ -9,7 +9,7 @@ thread_local! { Ssr::from( read_to_string("./client/dist/ssr/index.js").unwrap(), "SSR" - ) + ).unwrap() ) } @@ -31,7 +31,7 @@ async fn return_html(_req: Request<()>) -> tide::Result { println!("Elapsed: {:?}", start.elapsed()); let response = Response::builder(200) - .body(html) + .body(html.unwrap()) .content_type(tide::http::mime::HTML) .build(); diff --git a/examples/warp.rs b/examples/warp.rs index a55fe45..8520ede 100644 --- a/examples/warp.rs +++ b/examples/warp.rs @@ -10,7 +10,7 @@ thread_local! { Ssr::from( read_to_string("./client/dist/ssr/index.js").unwrap(), "SSR" - ) + ).unwrap() ) } @@ -22,7 +22,7 @@ async fn main() { let start = Instant::now(); let result = SSR.with(|ssr| ssr.borrow_mut().render_to_string(None)); println!("Elapsed: {:?}", start.elapsed()); - Response::builder().body(result) + Response::builder().body(result.unwrap()) }); let css = warp::path("styles").and(warp::fs::dir("./client/dist/ssr/styles/")); diff --git a/src/lib.rs b/src/lib.rs index c13a0f1..3aa642a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,9 @@ //! //! It use an embedded version of the v8 javascript engine (rusty_v8) to parse and evaluate a built bundle file and return a string with the rendered html. //! -//! Currently it works with Webpack bundler v4.44.2; check it out here a full project who use this crate. +//! Currently it works with Webpack bundler v4.44.2. //! -//! # Gettin started +//! # Getting started //! ```toml //! [dependencies] //! ssr_rs = "0.3.0" @@ -28,9 +28,9 @@ //! //! let source = read_to_string("./path/to/build.js").unwrap(); //! -//! let mut js = Ssr::from(source, "entryPoint"); +//! let mut js = Ssr::from(source, "entryPoint").unwrap(); //! -//! let html = js.render_to_string(None); +//! let html = js.render_to_string(None).unwrap(); //! //! assert_eq!(html, "...".to_string()); //! } @@ -56,14 +56,62 @@ //! //! let source = read_to_string("./path/to/build.js").unwrap(); //! -//! let mut js = Ssr::from(source, "entryPoint"); +//! let mut js = Ssr::from(source, "entryPoint").unwrap(); //! -//! let html = js.render_to_string(Some(&props)); +//! let html = js.render_to_string(Some(&props)).unwrap(); //! //! assert_eq!(html, "...".to_string()); //! } //!``` - +//! +//! # Example with actix-web +//! +//! > Examples with different web frameworks are available in the examples folder. +//! +//! Even though the V8 engine allows accessing the same `isolate` from different threads that is forbidden by this crate for two reasons: +//! 1. rusty_v8 library have not implemented yet the V8 Locker API. Accessing Ssr struct from a different thread will make the V8 engine to panic. +//! 2. Rendering HTML does not need shared state across threads. +//! +//! For this reason parallel computation is a better choice. Following actix-web setup: +//! +//! ```no_run +//! use actix_web::{get, http::StatusCode, App, HttpResponse, HttpServer}; +//! use std::cell::RefCell; +//! use std::fs::read_to_string; +//! +//! use ssr_rs::Ssr; +//! +//! thread_local! { +//! static SSR: RefCell> = RefCell::new( +//! Ssr::from( +//! read_to_string("./client/dist/ssr/index.js").unwrap(), +//! "SSR" +//! ).unwrap() +//! ) +//!} +//! +//! #[actix_web::main] +//!async fn main() -> std::io::Result<()> { +//! Ssr::create_platform(); +//! +//! HttpServer::new(|| { +//! App::new() +//! .service(index) +//! }) +//! .bind("127.0.0.1:8080")? +//! .run() +//! .await +//! } +//! +//! #[get("/")] +//! async fn index() -> HttpResponse { +//! let result = SSR.with(|ssr| ssr.borrow_mut().render_to_string(None).unwrap()); +//! +//! HttpResponse::build(StatusCode::OK) +//! .content_type("text/html; charset=utf-8") +//! .body(result) +//! } +//!``` mod ssr; pub use ssr::Ssr; diff --git a/src/ssr.rs b/src/ssr.rs index 08968bd..61943f5 100644 --- a/src/ssr.rs +++ b/src/ssr.rs @@ -1,6 +1,7 @@ // TODO: replace hashmap with more performant https://nnethercote.github.io/perf-book/hashing.html use std::collections::HashMap; +#[derive(Debug)] pub struct Ssr<'s, 'i> { isolate: *mut v8::OwnedIsolate, handle_scope: *mut v8::HandleScope<'s, ()>, @@ -43,7 +44,7 @@ where /// /// See the examples folder for more about using multiple parallel instances for multi-threaded /// execution. - pub fn from(source: String, entry_point: &str) -> Self { + pub fn from(source: String, entry_point: &str) -> Result { 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))) }; @@ -55,60 +56,78 @@ where let scope = unsafe { &mut *scope_ptr }; - let code = v8::String::new(scope, &format!("{source};{entry_point}")) - .expect("Invalid JS: Strings are needed"); + let code = match v8::String::new(scope, &format!("{source};{entry_point}")) { + Some(val) => val, + None => return Err("Invalid JS: Strings are needed"), + }; - let script = v8::Script::compile(scope, code, None) - .expect("Invalid JS: There aren't runnable scripts"); + let script = match v8::Script::compile(scope, code, None) { + Some(val) => val, + None => return Err("Invalid JS: There aren't runnable scripts"), + }; - let exports = script - .run(scope) - .expect("Invalid JS: Missing entry point. Is the bundle exported as a variable?"); + let exports = match script.run(scope) { + Some(val) => val, + None => { + return Err( + "Invalid JS: Missing entry point. Is the bundle exported as a variable?", + ) + } + }; - let object = exports - .to_object(scope) - .expect("Invalid JS: There are no objects"); + let object = match exports.to_object(scope) { + Some(val) => val, + None => return Err("Invalid JS: There are no objects"), + }; let mut fn_map: HashMap> = HashMap::new(); if let Some(props) = object.get_own_property_names(scope, Default::default()) { - fn_map = Some(props) + fn_map = match Some(props) .iter() .enumerate() - .map(|(i, &p)| { - let name = p - .get_index(scope, i as u32) - .expect("Failed to get function name"); - - let mut scope = v8::EscapableHandleScope::new(scope); - - let func = object - .get(&mut scope, name) - .expect("Failed to get function from obj"); - - let func = unsafe { v8::Local::::cast(func) }; - - ( - name.to_string(&mut scope) - .unwrap() - .to_rust_string_lossy(&mut scope), - scope.escape(func), - ) - }) + .map( + |(i, &p)| -> Result<(String, v8::Local), &'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::::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))) + }, + ) // TODO: collect directly the values into a map - .collect(); + .collect() + { + Ok(val) => val, + Err(err) => return Err(err), + } } - Ssr { + Ok(Ssr { isolate, handle_scope, fn_map, scope: scope_ptr, - } + }) } /// Execute the Javascript functions and return the result as string. - pub fn render_to_string(&mut self, params: Option<&str>) -> String { + pub fn render_to_string(&mut self, params: Option<&str>) -> Result { let scope = unsafe { &mut *self.scope }; let params: v8::Local = match v8::String::new(scope, params.unwrap_or("")) { @@ -122,18 +141,20 @@ where // TODO: transform this into an iterator for key in self.fn_map.keys() { - let result = self.fn_map[key] - .call(scope, undef, &[params]) - .expect("Failed to call function"); + let result = match self.fn_map[key].call(scope, undef, &[params]) { + Some(val) => val, + None => return Err("Failed to call function"), + }; - let result = result - .to_string(scope) - .expect("Failed to parse the result to string"); + 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)); } - rendered + Ok(rendered) } } @@ -151,21 +172,28 @@ mod tests { } #[test] - #[should_panic] fn wrong_entry_point() { init_test(); let source = r##"var entryPoint = {x: () => ""};"##; - let _ = Ssr::from(source.to_owned(), "IncorrectEntryPoint"); + let res = Ssr::from(source.to_owned(), "IncorrectEntryPoint"); + + assert_eq!( + res.unwrap_err(), + "Invalid JS: Missing entry point. Is the bundle exported as a variable?" + ); } #[test] - #[should_panic] fn empty_code() { init_test(); let source = r##""##; - let _ = Ssr::from(source.to_owned(), "SSR"); + let res = Ssr::from(source.to_owned(), "SSR"); + assert_eq!( + res.unwrap_err(), + "Invalid JS: Missing entry point. Is the bundle exported as a variable?" + ); } #[test] @@ -177,20 +205,20 @@ mod tests { 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"); + let mut js = Ssr::from(accept_params_source, "SSR").unwrap(); println!("Before render_to_string"); - let result = js.render_to_string(Some(&props)); + 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"); - let result2 = js2.render_to_string(Some(&props)); + 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); + let result3 = js.render_to_string(None).unwrap(); assert_eq!(result3, "These are our parameters: "); } @@ -201,16 +229,16 @@ mod tests { let source = r##"var SSR = {x: () => ""};"##.to_string(); - let mut js = Ssr::from(source, "SSR"); - let html = js.render_to_string(None); + let mut js = Ssr::from(source, "SSR").unwrap(); + let html = js.render_to_string(None).unwrap(); assert_eq!(html, ""); //Prevent missing semicolon let source2 = r##"var SSR = {x: () => ""}"##.to_string(); - let mut js2 = Ssr::from(source2, "SSR"); - let html2 = js2.render_to_string(None); + let mut js2 = Ssr::from(source2, "SSR").unwrap(); + let html2 = js2.render_to_string(None).unwrap(); assert_eq!(html2, ""); } @@ -222,19 +250,21 @@ mod tests { let mut js = Ssr::from( r##"var SSR = {x: () => ""};"##.to_string(), "SSR", - ); + ) + .unwrap(); - assert_eq!(js.render_to_string(None), ""); + assert_eq!(js.render_to_string(None).unwrap(), ""); assert_eq!( - js.render_to_string(Some(r#"{"Hello world"}"#)), + js.render_to_string(Some(r#"{"Hello world"}"#)).unwrap(), "" ); 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), "I don't accept params"); + assert_eq!(js2.render_to_string(None).unwrap(), "I don't accept params"); } }