dinja_core/renderer/
mod.rs

1//! JavaScript rendering engine using Deno Core
2//!
3//! This module provides functionality to render JavaScript components using
4//! the Deno Core engine with engine and engine-render-to-string loaded.
5//!
6//! ## Architecture
7//!
8//! The renderer module is organized into several submodules:
9//!
10//! - **`pool`**: Thread-local renderer pooling for performance optimization
11//! - **`runtime`**: JavaScript runtime lifecycle management and cleanup
12//! - **`scripts`**: JavaScript code generation for component rendering
13//! - **`engine`**: Static file loading and engine initialization
14//! - **`constants`**: Script tags and constants for runtime operations
15//!
16//! ## Thread Safety
17//!
18//! **Important**: `JsRenderer` uses `Rc<RefCell<JsRuntime>>` instead of `Arc<Mutex<JsRuntime>>`
19//! because `JsRuntime` is not `Send` or `Sync`. This means:
20//!
21//! - Renderers cannot be shared across threads
22//! - Each thread must create its own renderer instances
23//! - The renderer pool uses thread-local storage to maintain per-thread caches
24//!
25//! ## Usage
26//!
27//! ```no_run
28//! use dinja_core::renderer::JsRenderer;
29//! use std::path::Path;
30//!
31//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
32//! let renderer = JsRenderer::new("static")?;
33//! let html = renderer.render_component(
34//!     "function View(context = {{}}) {{ return engine.h('div', null, 'Hello'); }}",
35//!     Some(r#"{"name": "World"}"#)
36//! )?;
37//! # Ok(())
38//! # }
39//! ```
40
41mod constants;
42mod engine;
43pub mod pool;
44mod runtime;
45mod scripts;
46
47pub use pool::{RendererPool, RendererProfile};
48
49use crate::error::MdxError;
50use crate::models::ComponentDefinition;
51use anyhow::Result as AnyhowResult;
52use deno_core::{JsRuntime, RuntimeOptions};
53use std::cell::RefCell;
54use std::collections::HashMap;
55use std::path::Path;
56use std::rc::Rc;
57
58use constants::script_tags;
59use engine::load_static_files_internal;
60use runtime::{extract_string_from_v8, setup_context, with_runtime};
61use scripts::{
62    component_bootstrap_script, component_render_script, schema_render_script,
63    wrap_transformed_component,
64};
65
66/// A renderer that manages a Deno Core runtime with engine libraries loaded
67///
68/// Note: Uses `Rc<RefCell<JsRuntime>>` instead of `Arc<Mutex<JsRuntime>>` because
69/// `JsRuntime` is not `Send` or `Sync`, so it cannot be safely shared across threads.
70/// Each request handler creates its own renderer instance.
71pub struct JsRenderer {
72    runtime: Rc<RefCell<JsRuntime>>,
73}
74
75impl JsRenderer {
76    fn create_with_engine(static_dir: impl AsRef<Path>) -> AnyhowResult<Self> {
77        let mut runtime = JsRuntime::new(RuntimeOptions::default());
78
79        // Load static JavaScript files into the context
80        load_static_files_internal(&mut runtime, static_dir)?;
81
82        let renderer = Self {
83            runtime: Rc::new(RefCell::new(runtime)),
84        };
85
86        Ok(renderer)
87    }
88
89    /// Creates a new renderer instance and loads the static JavaScript files
90    ///
91    /// # Arguments
92    /// * `static_dir` - Path to the directory containing static JavaScript files
93    ///
94    /// # Returns
95    /// A new `JsRenderer` instance with libraries loaded
96    pub fn new(static_dir: impl AsRef<Path>) -> AnyhowResult<Self> {
97        Self::create_with_engine(static_dir)
98    }
99
100    /// Renders a JavaScript component to HTML string
101    ///
102    /// # Arguments
103    /// * `component_code` - JavaScript code that defines and exports a component
104    /// * `props` - Optional JSON string of props to pass to the component
105    ///
106    /// # Returns
107    /// Rendered HTML string
108    pub fn render_component(
109        &self,
110        component_code: &str,
111        props: Option<&str>,
112    ) -> AnyhowResult<String> {
113        let props_json = props.unwrap_or("{}");
114        with_runtime(Rc::clone(&self.runtime), |runtime| {
115            // Set up the context variable globally before executing component code
116            setup_context(runtime, props_json).map_err(anyhow::Error::from)?;
117
118            let render_script =
119                component_render_script(component_code, props_json).map_err(anyhow::Error::from)?;
120
121            // Evaluate and get the result
122            let result = runtime
123                .execute_script(script_tags::RENDER, render_script)
124                .map_err(|e| {
125                    anyhow::Error::from(MdxError::TsxTransform(format!(
126                        "Failed to render component: {e:?}"
127                    )))
128                })?;
129
130            extract_string_from_v8(result, runtime, "Failed to convert result to string")
131                .map_err(anyhow::Error::from)
132        })
133    }
134
135    /// Renders a JavaScript component using the transformed code from TSX
136    ///
137    /// # Arguments
138    /// * `transformed_js` - JavaScript code from TSX transformation
139    /// * `props` - Optional JSON string of props
140    /// * `components` - Optional map of component definitions to inject
141    ///
142    /// # Returns
143    /// Rendered HTML string
144    pub fn render_transformed_component(
145        &self,
146        transformed_js: &str,
147        props: Option<&str>,
148        components: Option<&HashMap<String, ComponentDefinition>>,
149    ) -> AnyhowResult<String> {
150        let component_bootstrap = component_bootstrap_script(components)?;
151        let wrapped_code = wrap_transformed_component(&component_bootstrap, transformed_js);
152
153        self.render_component(&wrapped_code, props)
154    }
155
156    /// Renders a JavaScript component to schema (JSON string) using core.js engine
157    ///
158    /// # Arguments
159    /// * `component_code` - JavaScript code that defines and exports a component
160    /// * `props` - Optional JSON string of props to pass to the component
161    ///
162    /// # Returns
163    /// Rendered schema as JSON string
164    pub fn render_component_to_schema(
165        &self,
166        component_code: &str,
167        props: Option<&str>,
168    ) -> AnyhowResult<String> {
169        let props_json = props.unwrap_or("{}");
170        with_runtime(Rc::clone(&self.runtime), |runtime| {
171            // Set up the context variable globally before executing component code
172            setup_context(runtime, props_json).map_err(anyhow::Error::from)?;
173
174            let render_script =
175                schema_render_script(component_code, props_json).map_err(anyhow::Error::from)?;
176
177            // Evaluate and get the result
178            let result = runtime
179                .execute_script(script_tags::RENDER, render_script)
180                .map_err(|e| {
181                    anyhow::Error::from(MdxError::TsxTransform(format!(
182                        "Failed to render component to schema: {e:?}"
183                    )))
184                })?;
185
186            extract_string_from_v8(result, runtime, "Failed to convert result to string")
187                .map_err(anyhow::Error::from)
188        })
189    }
190
191    /// Renders a JavaScript component to schema using the transformed code from TSX
192    ///
193    /// # Arguments
194    /// * `transformed_js` - JavaScript code from TSX transformation
195    /// * `props` - Optional JSON string of props
196    /// * `components` - Optional map of component definitions to inject
197    ///
198    /// # Returns
199    /// Rendered schema as JSON string
200    pub fn render_transformed_component_to_schema(
201        &self,
202        transformed_js: &str,
203        props: Option<&str>,
204        components: Option<&HashMap<String, ComponentDefinition>>,
205    ) -> AnyhowResult<String> {
206        let component_bootstrap = component_bootstrap_script(components)?;
207        let wrapped_code = wrap_transformed_component(&component_bootstrap, transformed_js);
208
209        self.render_component_to_schema(&wrapped_code, props)
210    }
211}
212
213impl Clone for JsRenderer {
214    fn clone(&self) -> Self {
215        Self {
216            runtime: Rc::clone(&self.runtime),
217        }
218    }
219}