use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RSCTree {
ClientReference {
id: String,
key: Option<String>,
props: FxHashMap<String, Value>,
},
ServerElement {
tag: String,
props: Option<FxHashMap<String, Value>>,
children: Option<Vec<RSCTree>>,
key: Option<String>,
},
Text(String),
Fragment {
children: Vec<RSCTree>,
key: Option<String>,
},
Array(Vec<RSCTree>),
Null,
Primitive(Value),
Error {
message: String,
component_name: String,
stack: Option<String>,
},
}
impl RSCTree {
pub fn client_reference(id: &str, key: Option<&str>, props: FxHashMap<String, Value>) -> Self {
RSCTree::ClientReference { id: id.to_string(), key: key.map(|k| k.to_string()), props }
}
pub fn server_element(
tag: &str,
props: Option<FxHashMap<String, Value>>,
children: Option<Vec<RSCTree>>,
key: Option<&str>,
) -> Self {
RSCTree::ServerElement {
tag: tag.to_string(),
props,
children,
key: key.map(|k| k.to_string()),
}
}
pub fn text(content: &str) -> Self {
RSCTree::Text(content.to_string())
}
pub fn fragment(children: Vec<RSCTree>, key: Option<&str>) -> Self {
RSCTree::Fragment { children, key: key.map(|k| k.to_string()) }
}
pub fn array(elements: Vec<RSCTree>) -> Self {
RSCTree::Array(elements)
}
pub fn null() -> Self {
RSCTree::Null
}
pub fn primitive(value: Value) -> Self {
RSCTree::Primitive(value)
}
pub fn error(message: &str, component_name: &str, stack: Option<&str>) -> Self {
RSCTree::Error {
message: message.to_string(),
component_name: component_name.to_string(),
stack: stack.map(|s| s.to_string()),
}
}
pub fn has_client_components(&self) -> bool {
match self {
RSCTree::ClientReference { .. } => true,
RSCTree::ServerElement { children, .. } => children
.as_ref()
.is_some_and(|c| c.iter().any(|child| child.has_client_components())),
RSCTree::Fragment { children, .. } => {
children.iter().any(|child| child.has_client_components())
}
RSCTree::Array(elements) => {
elements.iter().any(|element| element.has_client_components())
}
_ => false,
}
}
pub fn collect_client_component_ids(&self) -> Vec<String> {
let mut ids = Vec::new();
self.collect_client_component_ids_recursive(&mut ids);
ids
}
fn collect_client_component_ids_recursive(&self, ids: &mut Vec<String>) {
match self {
RSCTree::ClientReference { id, .. } => {
ids.push(id.clone());
}
RSCTree::ServerElement { children: Some(children), .. } => {
for child in children {
child.collect_client_component_ids_recursive(ids);
}
}
RSCTree::ServerElement { children: None, .. } => {
}
RSCTree::Fragment { children, .. } => {
for child in children {
child.collect_client_component_ids_recursive(ids);
}
}
RSCTree::Array(elements) => {
for element in elements {
element.collect_client_component_ids_recursive(ids);
}
}
_ => {}
}
}
pub fn to_json(&self) -> Value {
match self {
RSCTree::ClientReference { id, key, props } => {
#[allow(clippy::disallowed_methods)]
{
serde_json::json!(["$", id, key, props])
}
}
RSCTree::ServerElement { tag, props, children, key } => {
#[allow(clippy::disallowed_methods)]
let mut element = serde_json::json!({
"$$typeof": "react.transitional.element",
"type": tag,
"key": key,
"props": props.clone().unwrap_or_default()
});
if let Some(children) = children {
let children_json: Vec<Value> = children.iter().map(|c| c.to_json()).collect();
if let Some(props) = element.get_mut("props")
&& let Some(props_obj) = props.as_object_mut()
{
props_obj.insert("children".to_string(), Value::Array(children_json));
}
}
element
}
RSCTree::Text(content) => Value::String(content.clone()),
RSCTree::Fragment { children, .. } => {
let children_json: Vec<Value> = children.iter().map(|c| c.to_json()).collect();
Value::Array(children_json)
}
RSCTree::Array(elements) => {
let elements_json: Vec<Value> = elements.iter().map(|e| e.to_json()).collect();
Value::Array(elements_json)
}
RSCTree::Null => Value::Null,
RSCTree::Primitive(value) => value.clone(),
RSCTree::Error { message, component_name, .. } => {
#[allow(clippy::disallowed_methods)]
{
serde_json::json!({
"$$typeof": "react.transitional.element",
"type": "div",
"props": {
"style": {
"color": "red",
"border": "1px solid red",
"padding": "10px",
"margin": "10px"
},
"children": [
{
"$$typeof": "react.transitional.element",
"type": "h3",
"props": {
"children": format!("Error in {}", component_name)
}
},
{
"$$typeof": "react.transitional.element",
"type": "p",
"props": {
"children": message
}
}
]
}
})
}
}
}
}
pub fn from_json(value: &Value) -> Result<Self, String> {
match value {
Value::String(s) => Ok(RSCTree::Text(s.clone())),
Value::Number(n) => Ok(RSCTree::Primitive(Value::Number(n.clone()))),
Value::Bool(b) => Ok(RSCTree::Primitive(Value::Bool(*b))),
Value::Null => Ok(RSCTree::Null),
Value::Array(arr) => {
if arr.len() == 4 && arr[0] == "$" {
let id = arr[1].as_str().ok_or("Invalid reference ID")?;
let is_client_reference = id.starts_with("$L") || id.contains('#');
if is_client_reference {
let key = arr[2].as_str().map(|s| s.to_string());
let props = arr[3]
.as_object()
.ok_or("Invalid client reference props")?
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
Ok(RSCTree::ClientReference { id: id.to_string(), key, props })
} else {
let tag = id;
let key = arr[2].as_str().map(|s| s.to_string());
let props_obj = arr[3].as_object().ok_or("Invalid element props")?;
let mut props: FxHashMap<String, Value> =
props_obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
let children = props
.remove("children")
.map(|c| {
if c.is_array() {
c.as_array()
.ok_or("Invalid children array")?
.iter()
.map(RSCTree::from_json)
.collect::<Result<Vec<RSCTree>, String>>()
} else {
Ok(vec![RSCTree::from_json(&c)?])
}
})
.transpose()?;
Ok(RSCTree::ServerElement {
tag: tag.to_string(),
props: if props.is_empty() { None } else { Some(props) },
children,
key,
})
}
} else {
let elements: Result<Vec<RSCTree>, String> =
arr.iter().map(RSCTree::from_json).collect();
Ok(RSCTree::Array(elements?))
}
}
Value::Object(obj) => {
if obj.contains_key("~preSerializedSuspense") && obj.contains_key("rscArray") {
if let Some(rsc_array) = obj.get("rscArray") {
return RSCTree::from_json(rsc_array);
} else {
tracing::error!(
"Pre-serialized Suspense marker found but rscArray is missing or invalid"
);
return Ok(RSCTree::Primitive(value.clone()));
}
}
if obj.contains_key("$$typeof") && obj.contains_key("type") {
let tag =
obj.get("type").and_then(|t| t.as_str()).ok_or("Invalid element type")?;
let key = obj.get("key").and_then(|k| k.as_str()).map(|s| s.to_string());
let props = obj
.get("props")
.and_then(|p| p.as_object())
.map(|p| p.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default();
let children = obj
.get("props")
.and_then(|p| p.get("children"))
.map(|c| {
if c.is_array() {
c.as_array()
.ok_or("Invalid children array")?
.iter()
.map(RSCTree::from_json)
.collect::<Result<Vec<RSCTree>, String>>()
} else {
Ok(vec![RSCTree::from_json(c)?])
}
})
.transpose()?;
Ok(RSCTree::ServerElement {
tag: tag.to_string(),
props: Some(props),
children,
key,
})
} else {
Ok(RSCTree::Primitive(value.clone()))
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RSCRenderResult {
pub tree: RSCTree,
pub has_suspense: bool,
pub client_components: Vec<String>,
pub debug: Option<RSCRenderDebug>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RSCRenderDebug {
pub component_id: String,
pub success: bool,
pub error: Option<String>,
pub has_client_components: bool,
pub resolved_from_suspense: bool,
pub render_type: String,
}
impl RSCRenderResult {
pub fn success(tree: RSCTree, component_id: &str) -> Self {
let client_components = tree.collect_client_component_ids();
let has_client_components = !client_components.is_empty();
RSCRenderResult {
tree,
has_suspense: false,
client_components,
debug: Some(RSCRenderDebug {
component_id: component_id.to_string(),
success: true,
error: None,
has_client_components,
resolved_from_suspense: false,
render_type: "rsc_tree".to_string(),
}),
}
}
pub fn error(error: &str, component_id: &str) -> Self {
RSCRenderResult {
tree: RSCTree::error(error, component_id, None),
has_suspense: false,
client_components: vec![],
debug: Some(RSCRenderDebug {
component_id: component_id.to_string(),
success: false,
error: Some(error.to_string()),
has_client_components: false,
resolved_from_suspense: false,
render_type: "error".to_string(),
}),
}
}
pub fn with_suspense(mut self) -> Self {
self.has_suspense = true;
if let Some(debug) = &mut self.debug {
debug.resolved_from_suspense = true;
}
self
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_client_reference_creation() {
let mut props = FxHashMap::default();
props.insert("onClick".to_string(), json!("handleClick"));
let tree = RSCTree::client_reference("Counter", Some("key1"), props);
match tree {
RSCTree::ClientReference { id, key, props } => {
assert_eq!(id, "Counter");
assert_eq!(key, Some("key1".to_string()));
assert_eq!(props.get("onClick"), Some(&json!("handleClick")));
}
_ => panic!("Expected ClientReference"),
}
}
#[test]
fn test_server_element_creation() {
let children = vec![RSCTree::text("Hello World")];
let mut props = FxHashMap::default();
props.insert("className".to_string(), json!("container"));
let tree = RSCTree::server_element("div", Some(props), Some(children), None);
match tree {
RSCTree::ServerElement { tag, props, children, .. } => {
assert_eq!(tag, "div");
assert_eq!(props.as_ref().unwrap().get("className"), Some(&json!("container")));
assert_eq!(children.as_ref().unwrap().len(), 1);
}
_ => panic!("Expected ServerElement"),
}
}
#[test]
fn test_has_client_components() {
let client_ref = RSCTree::client_reference("Counter", None, FxHashMap::default());
assert!(client_ref.has_client_components());
let text = RSCTree::text("Hello");
assert!(!text.has_client_components());
let server_with_client = RSCTree::server_element("div", None, Some(vec![client_ref]), None);
assert!(server_with_client.has_client_components());
}
#[test]
fn test_json_serialization() {
let mut props = FxHashMap::default();
props.insert("count".to_string(), json!(42));
let tree = RSCTree::client_reference("Counter", None, props);
let json_value = tree.to_json();
assert_eq!(json_value[0], "$");
assert_eq!(json_value[1], "Counter");
assert_eq!(json_value[3]["count"], 42);
}
#[test]
fn test_json_deserialization() {
let json_value = json!(["$", "$L0", null, {"count": 42}]);
let tree = RSCTree::from_json(&json_value).unwrap();
match tree {
RSCTree::ClientReference { id, key, props } => {
assert_eq!(id, "$L0");
assert_eq!(key, None);
assert_eq!(props.get("count"), Some(&json!(42)));
}
_ => panic!("Expected ClientReference"),
}
}
}