use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use crate::language::LanguageEngine;
use crate::rwe::interface::ReactiveWebEngine;
use crate::rwe::model::{
CompiledScript, CompiledScriptScope, CompiledTemplate, ReactiveBinding, ReactiveMode,
ReactiveWebDiagnostic, ReactiveWebError, ReactiveWebOptions, RenderContext, RenderOutput,
RuntimeBundle, RuntimeMode, TemplateSource,
};
use crate::rwe::processors;
#[derive(Default)]
pub struct RweReactiveWebEngine;
impl ReactiveWebEngine for RweReactiveWebEngine {
fn id(&self) -> &'static str {
"rwe"
}
fn compile_template(
&self,
template: &TemplateSource,
_language: &dyn LanguageEngine,
options: &ReactiveWebOptions,
) -> Result<CompiledTemplate, ReactiveWebError> {
let compile_options = crate::rwe::core::CompileOptions {
template_root: options
.templates
.template_root
.as_ref()
.map(|p| p.display().to_string()),
library_roots: options
.templates
.library_roots
.iter()
.map(|(k, v)| (k.clone(), v.display().to_string()))
.collect(),
file_path: template
.source_path
.as_ref()
.map(|p| p.display().to_string()),
runtime_mode: match options.runtime_mode {
RuntimeMode::Dev | RuntimeMode::Prod => crate::rwe::core::RuntimeMode::Inline,
},
security: crate::rwe::core::SecurityPolicy {
import_allowlist: {
let mut allow = vec!["@/".to_string()];
allow.extend(options.allow_list.urls.clone());
allow
},
network_allowlist: if options.allow_list.urls.is_empty() {
vec!["registry.npmjs.org".to_string(), "jsr.io".to_string()]
} else {
options.allow_list.urls.clone()
},
blocked_globals: vec![
"eval".to_string(),
"Function".to_string(),
"globalThis.Function".to_string(),
],
allow_dynamic_import: !options.strict_mode,
allow_raw_html: false,
},
deno_timeout_ms: 3_000,
};
let compiled =
crate::rwe::core::compile(&template.markup, compile_options).map_err(|err| {
ReactiveWebError::new(
"RWE_COMPILE",
format!("rwe compile failed for '{}': {}", template.id, err.message),
)
})?;
let warmup_enabled = std::env::var("ZEBFLOW_RWE_PREWARM")
.map(|v| v != "0")
.unwrap_or(true);
if warmup_enabled {
let warm_compiled = compiled.clone();
std::thread::spawn(move || {
let _ = crate::rwe::core::prewarm(&warm_compiled);
});
}
let payload = serde_json::to_value(&compiled).map_err(|err| {
ReactiveWebError::new(
"RWE_PAYLOAD",
format!("failed serializing rwe payload: {err}"),
)
})?;
let diagnostics = compiled
.diagnostics
.iter()
.map(|d| ReactiveWebDiagnostic {
code: d.code.clone(),
message: d.message.clone(),
})
.collect::<Vec<_>>();
Ok(CompiledTemplate {
engine_id: self.id().to_string(),
template_id: template.id.clone(),
html_ir: "<!-- rwe html plan -->".to_string(),
control_script_source: None,
compiled_logic: None,
runtime_bundle: RuntimeBundle {
name: "rwe-runtime".to_string(),
source: String::new(),
},
reactive_bindings: if options.reactive_mode == ReactiveMode::Bindings {
vec![ReactiveBinding {
kind: "rwe".to_string(),
key: "event-plan".to_string(),
}]
} else {
Vec::new()
},
diagnostics,
needs_runtime_tailwind_rebuild: false,
tailwind_variant_exact_tokens: Vec::new(),
tailwind_variant_patterns: Vec::new(),
options: options.clone(),
engine_payload: Some(payload),
dependency_paths: compiled.dependency_paths.clone(),
})
}
fn render(
&self,
compiled: &CompiledTemplate,
state: Value,
_language: &dyn LanguageEngine,
ctx: &RenderContext,
) -> Result<RenderOutput, ReactiveWebError> {
let payload = compiled.engine_payload.as_ref().ok_or_else(|| {
ReactiveWebError::new(
"RWE_PAYLOAD_MISSING",
"compiled template missing rwe engine payload",
)
})?;
let rwe_compiled: crate::rwe::core::CompiledTemplate =
serde_json::from_value(payload.clone()).map_err(|err| {
ReactiveWebError::new(
"RWE_PAYLOAD_PARSE",
format!("failed parsing rwe payload: {err}"),
)
})?;
let mut state = state;
if let Some(object) = state.as_object_mut() {
object
.entry("route")
.or_insert_with(|| Value::String(ctx.route.clone()));
}
let rendered = crate::rwe::core::render(&rwe_compiled, &state, &ctx.enabled_libraries)
.map_err(|err| {
ReactiveWebError::new("RWE_RENDER", format!("rwe render failed: {}", err.message))
})?;
let mut processor_diags: Vec<ReactiveWebDiagnostic> = Vec::new();
let processed_html = processors::apply_compile_processors(
&rendered.html,
&rwe_compiled.server_module_source,
&compiled.options,
&mut processor_diags,
);
let (clean_html, extracted_css) = extract_generated_tailwind_style(&processed_html);
let mut scripts = Vec::new();
if !rendered.js.is_empty() {
let hash = stable_hash_hex(&rendered.js);
let script_id = format!("rwe.page.{}", compiled.template_id);
scripts.push(CompiledScript {
id: script_id,
scope: CompiledScriptScope::Page,
content_type: "text/javascript".to_string(),
content: rendered.js,
content_hash: hash.clone(),
suggested_file_name: format!("rwe-{hash}.mjs"),
});
}
Ok(RenderOutput {
html: clean_html,
compiled_scripts: scripts,
hydration_payload: json!({
"engine": "rwe",
"css": extracted_css,
"meta": rendered.meta,
"processor_diagnostics": processor_diags,
}),
trace: vec!["rwe.render".to_string()],
})
}
}
fn stable_hash_hex(input: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let digest = hasher.finalize();
hex::encode(digest)
}
fn extract_generated_tailwind_style(html: &str) -> (String, String) {
const OPEN: &str = "<style data-rwe-tw>";
const CLOSE: &str = "</style>";
if let Some(start) = html.find(OPEN) {
let content_start = start + OPEN.len();
if let Some(end_rel) = html[content_start..].find(CLOSE) {
let end = content_start + end_rel;
let css = html[content_start..end].to_string();
let mut out = String::with_capacity(html.len());
out.push_str(&html[..start]);
out.push_str(&html[end + CLOSE.len()..]);
return (out, css);
}
}
(html.to_string(), String::new())
}