pub mod compiler;
pub mod config;
pub mod deno_worker;
pub mod error;
pub mod js_masker;
pub mod model;
pub mod render;
pub mod security;
pub mod zeb_react;
pub use config::{CompileOptions, RuntimeMode, SecurityPolicy};
pub use error::EngineError;
pub use model::{CompiledTemplate, RenderOutput};
pub fn compile(source: &str, options: CompileOptions) -> Result<CompiledTemplate, EngineError> {
compiler::compile(source, options)
}
pub fn render(
compiled: &CompiledTemplate,
vars: &serde_json::Value,
enabled_libraries: &[String],
) -> Result<RenderOutput, EngineError> {
render::render(compiled, vars, enabled_libraries)
}
pub fn prewarm(compiled: &CompiledTemplate) -> Result<(), EngineError> {
render::prewarm(compiled)
}
pub fn prepare_template_root(root: &std::path::Path) -> Result<(), EngineError> {
use std::fs;
use std::path::PathBuf;
const RWE_SHIM: &str = include_str!("../runtime/rwe.ts");
let shim_path = root.join("rwe.ts");
write_atomic(&shim_path, RWE_SHIM.as_bytes()).map_err(|e| {
EngineError::new("RWE_PREPARE_SHIM", format!("failed writing rwe shim: {e}"))
})?;
fn collect(dir: &std::path::Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect(&path, out)?;
} else if let Some(ext) = path.extension().and_then(|v| v.to_str()) {
if matches!(ext, "tsx" | "ts" | "jsx" | "js") {
out.push(path);
}
}
}
Ok(())
}
let mut files = Vec::new();
collect(root, &mut files).map_err(|e| {
EngineError::new(
"RWE_PREPARE_COLLECT",
format!("failed collecting template files: {e}"),
)
})?;
for file in files {
if file == shim_path {
continue; }
let source = fs::read_to_string(&file).map_err(|e| {
EngineError::new(
"RWE_PREPARE_READ",
format!("failed reading '{}': {e}", file.display()),
)
})?;
let rewritten = rewrite_source(&source, root)?;
if rewritten != source {
write_atomic(&file, rewritten.as_bytes()).map_err(|e| {
EngineError::new(
"RWE_PREPARE_WRITE",
format!("failed writing '{}': {e}", file.display()),
)
})?;
}
}
Ok(())
}
pub fn write_atomic(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let name = path
.file_name()
.and_then(|v| v.to_str())
.unwrap_or("file");
let tmp = path.with_file_name(format!(".{name}.{}.{seq}.tmp", std::process::id()));
std::fs::write(&tmp, bytes)?;
std::fs::rename(&tmp, path)
}
fn rewrite_source(source: &str, root: &std::path::Path) -> Result<String, EngineError> {
let mut out = source.to_string();
out = rewrite_imports_variant(&out, root, '"')?;
out = rewrite_imports_variant(&out, root, '\'')?;
Ok(out)
}
fn rewrite_imports_variant(
source: &str,
root: &std::path::Path,
quote: char,
) -> Result<String, EngineError> {
use std::fs;
let marker = format!("from {quote}");
let mut out = source.to_string();
let mut cursor = 0usize;
while let Some(rel_idx) = out[cursor..].find(marker.as_str()) {
let idx = cursor + rel_idx;
let spec_start = idx + marker.len();
let Some(end_rel) = out[spec_start..].find(quote) else {
break;
};
let spec_end = spec_start + end_rel;
let rel = &out[spec_start..spec_end];
if rel == "zeb" {
return Err(EngineError::new(
"RWE_IMPORT_NOT_ALLOWED",
"import from 'zeb' is not allowed; hooks and helpers come from \"zeb/react\" \
— write `from \"zeb/react\"` instead"
.to_string(),
));
}
if rel == "rwe" {
let shim = root.join("rwe.ts");
let resolved = fs::canonicalize(&shim).unwrap_or(shim);
let resolved_str = resolved.to_string_lossy().to_string();
out.replace_range(spec_start..spec_end, &resolved_str);
cursor = spec_start + resolved_str.len();
continue;
}
if !rel.starts_with("@/") {
cursor = spec_end + 1;
continue;
}
let resolved = resolve_alias(root, rel.trim_start_matches("@/"))?;
out.replace_range(spec_start..spec_end, &resolved);
cursor = spec_start + resolved.len();
}
Ok(out)
}
fn resolve_alias(root: &std::path::Path, rel: &str) -> Result<String, EngineError> {
use std::fs;
use std::path::PathBuf;
let base = root.join(rel);
if base.exists() {
let abs = fs::canonicalize(&base).unwrap_or(base);
return Ok(abs.to_string_lossy().to_string());
}
for ext in [".tsx", ".ts", ".jsx", ".js"] {
let candidate = PathBuf::from(format!("{}{ext}", base.display()));
if candidate.exists() {
let abs = fs::canonicalize(&candidate).unwrap_or(candidate);
return Ok(abs.to_string_lossy().to_string());
}
}
for index in ["index.tsx", "index.ts", "index.jsx", "index.js"] {
let candidate = base.join(index);
if candidate.exists() {
let abs = fs::canonicalize(&candidate).unwrap_or(candidate);
return Ok(abs.to_string_lossy().to_string());
}
}
Ok(base.to_string_lossy().to_string())
}