Hotreloading of for/if/body, formatted strings, literals, component props, nested rsx, light CLI rewrite, cli TUI (#2258)

Hotreload the contents of for loops, if chains, component bodies, props, attributes, and any literals discovered in rsx!
Add a TUI renderer to the CLI.
Improve the CLI build system to be async and parallel.
Refactor RSX to allow partial expansion of expressions.
Merge autofmt implementations for consistency.
Merge the representation of elements and components under the hood.
Add a diagnostics system for rsx for improved error messages.
Drop interprocess and move to websockets for communication between the CLI and the server.
Assign IDs to nodes and attributes in a stable way to be used in non compiler contexts.
Add hotreloading to any body of component/for loop/if chain/etc.

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
Co-authored-by: Liam Mitchell <liamkarlmitchell@gmail.com>
This commit is contained in:
authored and GitHub committed 2024-07-17 19:11:18 -07:00
1 parent 176e67e5b7
commit 88af3e7eff
181 files changed
+12755 -10806

No files matched your search

+381
View File
@@ -0,0 +1,381 @@
//! I'm so sorry this is so complicated. Here's my best to simplify and explain it:
//!
//! The `Callbody` is the contents of the rsx! macro - this contains all the information about every
//! node that rsx! directly knows about. For loops, if statements, etc.
//!
//! However, thre are multiple *templates* inside a callbody - due to how core clones templates and
//! just generally rationalize the concept of a template, nested bodies like for loops and if statements
//! and component children are all templates, contained within the same Callbody.
//!
//! This gets confusing fast since there's lots of IDs bouncing around.
//!
//! The IDs at play:
//! - The id of the template itself so we can find it and apply it to the dom.
//! This is challenging since all calls to file/line/col/id are relative to the macro invocation,
//! so they will have to share the same base ID and we need to give each template a new ID.
//! The id of the template will be something like file!():line!():col!():ID where ID increases for
//! each nested template.
//!
//! - The IDs of dynamic nodes relative to the template they live in. This is somewhat easy to track
//! but needs to happen on a per-template basis.
//!
//! - The unique ID of a hotreloadable literal (like ifmt or integers or strings, etc). This ID is
//! unique to the Callbody, not necessarily the template it lives in. This is similar to the
//! template ID
//!
//! We solve this by parsing the structure completely and then doing a second pass that fills in IDs
//! by walking the structure.
//!
//! This means you can't query the ID of any node "in a vacuum" - these are assigned once - but at
//! least they're stable enough for the purposes of hotreloading
//!
//! The plumbing for hotreloadable literals could be template relative... ie "file:line:col:template:idx"
//! That would be ideal if we could determine the the idx only relative to the template
//!
//! ```rust, ignore
//! rsx! {
//! div {
//! class: "hello",
//! id: "node-{node_id}", <--- hotreloadable with ID 0
//! ..props, <--- spreads are not reloadable
//!
//! "Hello, world! <--- not tracked but reloadable since it's just a string
//!
//! for item in 0..10 { <--- both 0 and 10 are technically reloadable...
//! div { "cool-{item}" } <--- the ifmt here is also reloadable
//! }
//!
//! Link {
//! to: "/home", <-- hotreloadable since its a component prop
//! class: "link {is_ready}", <-- hotreloadable since its a formatted string as a prop
//! "Home" <-- hotreloadable since its a component child (via template)
//! }
//! }
//! }
//! ```
use self::location::DynIdx;
use crate::innerlude::Attribute;
use crate::*;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro2_diagnostics::SpanDiagnosticExt;
use syn::token::Brace;
#[cfg(feature = "hot_reload")]
use dioxus_core::prelude::Template;
type NodePath = Vec<u8>;
type AttributePath = Vec<u8>;
/// A set of nodes in a template position
///
/// this could be:
/// - The root of a callbody
/// - The children of a component
/// - The children of a for loop
/// - The children of an if chain
///
/// The TemplateBody when needs to be parsed into a surrounding `Body` to be correctly re-indexed
/// By default every body has a `0` default index
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct TemplateBody {
pub roots: Vec<BodyNode>,
pub template_idx: DynIdx,
pub node_paths: Vec<NodePath>,
pub attr_paths: Vec<(AttributePath, usize)>,
pub diagnostic: Diagnostics,
current_path: Vec<u8>,
}
impl Parse for TemplateBody {
/// Parse the nodes of the callbody as `Body`.
fn parse(input: ParseStream) -> Result<Self> {
let brace = Brace::default();
let RsxBlock {
brace: _,
attributes,
spreads,
children,
diagnostics: _, // we don't care about the diagnostics here - ours might be different
} = RsxBlock::parse_inner(input, brace)?;
let mut template = Self::new(children);
for spread in spreads {
template.diagnostic.push(
spread
.span()
.error("Spreads are only allowed in elements and components"),
);
}
for attr in attributes {
template.diagnostic.push(
attr.span()
.error("Attributes are only allowed in elements and components"),
);
}
Ok(template)
}
}
/// Our ToTokens impl here just defers to rendering a template out like any other `Body`.
/// This is because the parsing phase filled in all the additional metadata we need
impl ToTokens for TemplateBody {
fn to_tokens(&self, tokens: &mut TokenStream2) {
// If there are no roots, this is an empty template, so just return None
if self.roots.is_empty() {
return tokens.append_all(quote! { dioxus_core::VNode::empty() });
}
// If we have an implicit key, then we need to write its tokens
let key_tokens = match self.implicit_key() {
Some(tok) => quote! { Some( #tok.to_string() ) },
None => quote! { None },
};
let TemplateBody { roots, .. } = self;
let roots = roots.iter().map(|node| match node {
BodyNode::Element(el) => quote! { #el },
BodyNode::Text(text) if text.is_static() => {
let text = text.input.to_static().unwrap();
quote! { dioxus_core::TemplateNode::Text { text: #text } }
}
BodyNode::Text(text) => {
let id = text.dyn_idx.get();
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
}
BodyNode::ForLoop(floop) => {
let id = floop.dyn_idx.get();
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
}
BodyNode::RawExpr(exp) => {
let id = exp.dyn_idx.get();
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
}
BodyNode::Component(exp) => {
let id = exp.dyn_idx.get();
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
}
BodyNode::IfChain(exp) => {
let id = exp.dyn_idx.get();
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
}
});
// Print paths is easy - just print the paths
let node_paths = self.node_paths.iter().map(|it| quote!(&[#(#it),*]));
let attr_paths = self.attr_paths.iter().map(|(it, _)| quote!(&[#(#it),*]));
// For printing dynamic nodes, we rely on the ToTokens impl
// Elements have a weird ToTokens - they actually are the entrypoint for Template creation
let dynamic_nodes = self.node_paths.iter().map(|path| {
let node = self.get_dyn_node(path);
quote::quote! { #node }
});
// We could add a ToTokens for Attribute but since we use that for both components and elements
// They actually need to be different, so we just localize that here
let dyn_attr_printer = self
.attr_paths
.iter()
.map(|(path, idx)| self.get_dyn_attr(path, *idx).rendered_as_dynamic_attr());
// Rust analyzer will not autocomplete properly if we change the name every time you type a character
// If it looks like we are running in rust analyzer, we'll just use a placeholder location
// let looks_like_rust_analyzer = first_root_span.contains("SpanData");
// let index = if looks_like_rust_analyzer {
// "0".to_string()
// } else {
// self.template_idx.get().to_string()
// };
// todo: this just might be fixed?
let index = self.template_idx.get();
tokens.append_all(quote! {
dioxus_core::Element::Ok({
#[doc(hidden)] // vscode please stop showing these in symbol search
static ___TEMPLATE: dioxus_core::Template = dioxus_core::Template {
name: concat!( file!(), ":", line!(), ":", column!(), ":", #index ) ,
roots: &[ #( #roots ),* ],
node_paths: &[ #( #node_paths ),* ],
attr_paths: &[ #( #attr_paths ),* ],
};
{
// NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
#[allow(clippy::let_and_return)]
let __vnodes = dioxus_core::VNode::new(
#key_tokens,
___TEMPLATE,
Box::new([ #( #dynamic_nodes),* ]),
Box::new([ #( #dyn_attr_printer ),* ]),
);
__vnodes
}
})
});
}
}
impl TemplateBody {
/// Create a new TemplateBody from a set of nodes
///
/// This will fill in all the necessary path information for the nodes in the template and will
/// overwrite data like dynamic indexes.
pub fn new(nodes: Vec<BodyNode>) -> Self {
let mut body = Self {
roots: vec![],
template_idx: DynIdx::default(),
node_paths: Vec::new(),
attr_paths: Vec::new(),
current_path: Vec::new(),
diagnostic: Diagnostics::new(),
};
// Assign paths to all nodes in the template
body.assign_paths_inner(&nodes);
// And then save the roots
body.roots = nodes;
body
}
/// Cascade down path information into the children of this template
///
/// This provides the necessary path and index information for the children of this template
/// so that they can render out their dynamic nodes correctly. Also does plumbing for things like
/// hotreloaded literals which need to be tracked on a per-template basis.
///
/// This can only operate with knowledge of this template, not the surrounding callbody. Things like
/// wiring of ifmt literals need to be done at the callbody level since those final IDs need to
/// be unique to the entire app.
fn assign_paths_inner(&mut self, nodes: &[BodyNode]) {
for (idx, node) in nodes.iter().enumerate() {
self.current_path.push(idx as u8);
match node {
// Just descend into elements - they're not dynamic
BodyNode::Element(el) => {
for (idx, attr) in el.merged_attributes.iter().enumerate() {
if !attr.is_static_str_literal() {
attr.dyn_idx.set(self.attr_paths.len());
self.attr_paths.push((self.current_path.clone(), idx));
}
}
self.assign_paths_inner(&el.children)
}
// Text nodes are dynamic if they contain dynamic segments
BodyNode::Text(txt) => {
if !txt.is_static() {
self.assign_path_to(node);
}
}
// Raw exprs are always dynamic
BodyNode::RawExpr(_)
| BodyNode::ForLoop(_)
| BodyNode::Component(_)
| BodyNode::IfChain(_) => self.assign_path_to(node),
};
self.current_path.pop();
}
}
/// Assign a path to a node and give it its dynamic index
/// This simplifies the ToTokens implementation for the macro to be a little less centralized
fn assign_path_to(&mut self, node: &BodyNode) {
// Assign the TemplateNode::Dynamic index to the node
node.set_dyn_idx(self.node_paths.len());
// And then save the current path as the corresponding path
self.node_paths.push(self.current_path.clone());
}
/// Create a new template from this TemplateBody
///
/// Note that this will leak memory! We explicitly call `leak` on the vecs to match the format of
/// the `Template` struct.
#[cfg(feature = "hot_reload")]
pub fn to_template<Ctx: HotReloadingContext>(&self) -> Template {
self.to_template_with_custom_paths::<Ctx>(
"placeholder",
self.node_paths.clone(),
self.attr_paths.clone().into_iter().map(|v| v.0).collect(),
)
}
#[cfg(feature = "hot_reload")]
pub fn to_template_with_custom_paths<Ctx: HotReloadingContext>(
&self,
location: &'static str,
node_paths: Vec<NodePath>,
attr_paths: Vec<AttributePath>,
) -> Template {
let roots = self
.roots
.iter()
.map(|node| node.to_template_node::<Ctx>())
.collect::<Vec<_>>();
Template {
name: location,
roots: intern(roots.as_slice()),
node_paths: intern(
node_paths
.into_iter()
.map(|path| intern(path.as_slice()))
.collect::<Vec<_>>()
.as_slice(),
),
attr_paths: intern(
attr_paths
.into_iter()
.map(|path| intern(path.as_slice()))
.collect::<Vec<_>>()
.as_slice(),
),
}
}
pub fn is_empty(&self) -> bool {
self.roots.is_empty()
}
fn implicit_key(&self) -> Option<IfmtInput> {
match self.roots.first() {
Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key().cloned(),
Some(BodyNode::Component(comp)) if self.roots.len() == 1 => {
comp.get_key().and_then(|f| f.ifmt().cloned())
}
_ => None,
}
}
pub fn get_dyn_node(&self, path: &[u8]) -> &BodyNode {
let mut node = self.roots.get(path[0] as usize).unwrap();
for idx in path.iter().skip(1) {
node = node.element_children().get(*idx as usize).unwrap();
}
node
}
pub fn get_dyn_attr(&self, path: &AttributePath, idx: usize) -> &Attribute {
match self.get_dyn_node(path) {
BodyNode::Element(el) => &el.merged_attributes[idx],
_ => unreachable!(),
}
}
pub fn dynamic_attributes(&self) -> impl DoubleEndedIterator<Item = &Attribute> {
self.attr_paths
.iter()
.map(|(path, idx)| self.get_dyn_attr(path, *idx))
}
pub fn dynamic_nodes(&self) -> impl DoubleEndedIterator<Item = &BodyNode> {
self.node_paths.iter().map(|path| self.get_dyn_node(path))
}
}