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}