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:
181 files changed
+12755
-10806
No files matched your search
@@ -103,64 +103,17 @@ impl VirtualDom {
|
||||
to: &mut impl WriteMutations,
|
||||
mut template: Template,
|
||||
) {
|
||||
// In debug mode, we check the more complete hashmap by byte index
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
|
||||
|
||||
let byte_index = byte_index.parse::<usize>().unwrap();
|
||||
let mut entry = self.templates.entry(path);
|
||||
// If we've already seen this template, just return
|
||||
if let std::collections::hash_map::Entry::Occupied(occupied) = &entry {
|
||||
if occupied.get().contains_key(&byte_index) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, insert it and register it
|
||||
entry.or_default().insert(byte_index, template);
|
||||
}
|
||||
|
||||
// In release mode, everything is built into the &'static str
|
||||
#[cfg(not(debug_assertions))]
|
||||
if !self.templates.insert(template.name) {
|
||||
if self.templates.contains_key(template.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = self.templates.insert(template.name, template);
|
||||
|
||||
// If it's all dynamic nodes, then we don't need to register it
|
||||
if !template.is_completely_dynamic() {
|
||||
to.register_template(template)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
/// Insert a new template into the VirtualDom's template registry
|
||||
pub(crate) fn register_template_first_byte_index(&mut self, mut template: Template) {
|
||||
// First, make sure we mark the template as seen, regardless if we process it
|
||||
let (path, _) = template.name.rsplit_once(':').unwrap();
|
||||
if let Some((_, old_template)) = self
|
||||
.templates
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.iter_mut()
|
||||
.min_by_key(|(byte_index, _)| **byte_index)
|
||||
{
|
||||
// the byte index of the hot reloaded template could be different
|
||||
template.name = old_template.name;
|
||||
*old_template = template;
|
||||
} else {
|
||||
// This is a template without any current instances
|
||||
self.templates
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.insert(usize::MAX, template);
|
||||
}
|
||||
|
||||
// If it's all dynamic nodes, then we don't need to register it
|
||||
if !template.is_completely_dynamic() {
|
||||
self.queued_templates.push(template);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We can apply various optimizations to dynamic nodes that are the single child of their parent.
|
||||
|
||||
@@ -24,17 +24,14 @@ impl VNode {
|
||||
// If hot reloading is enabled, we need to make sure we're using the latest template
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let (path, byte_index) = new.template.get().name.rsplit_once(':').unwrap();
|
||||
if let Some(map) = dom.templates.get(path) {
|
||||
let byte_index = byte_index.parse::<usize>().unwrap();
|
||||
if let Some(&template) = map.get(&byte_index) {
|
||||
new.template.set(template);
|
||||
if template != self.template.get() {
|
||||
let mount_id = self.mount.get();
|
||||
let parent = dom.mounts[mount_id.0].parent;
|
||||
self.replace(std::slice::from_ref(new), parent, dom, to);
|
||||
return;
|
||||
}
|
||||
let name = new.template.get().name;
|
||||
if let Some(template) = dom.templates.get(name).cloned() {
|
||||
new.template.set(template);
|
||||
if template != self.template.get() {
|
||||
let mount_id = self.mount.get();
|
||||
let parent = dom.mounts[mount_id.0].parent;
|
||||
self.replace(std::slice::from_ref(new), parent, dom, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -576,12 +573,7 @@ impl VNode {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let template = self.template.get();
|
||||
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
|
||||
if let Some(new_template) = dom
|
||||
.templates
|
||||
.get(path)
|
||||
.and_then(|map| map.get(&byte_index.parse().unwrap()))
|
||||
{
|
||||
if let Some(new_template) = dom.templates.get(template.name) {
|
||||
self.template.set(*new_template);
|
||||
}
|
||||
};
|
||||
@@ -625,6 +617,7 @@ impl VNode {
|
||||
|
||||
// Walk the roots, creating nodes and assigning IDs
|
||||
// nodes in an iterator of (dynamic_node_index, path)
|
||||
|
||||
let nodes_sorted = template.breadth_first_node_paths();
|
||||
let attrs_sorted = template.breadth_first_attribute_paths();
|
||||
|
||||
@@ -811,6 +804,7 @@ impl VNode {
|
||||
dynamic_attrbiutes_iter.next_if(from_root_node)
|
||||
{
|
||||
let attribute = &self.dynamic_attrs[attribute_idx];
|
||||
|
||||
let id = match last_path {
|
||||
// If the last path was exactly the same, we can reuse the id
|
||||
Some((path, id)) if path == attribute_path => id,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct HotreloadedLiteral {
|
||||
pub name: String,
|
||||
pub value: HotReloadLiteral,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum HotReloadLiteral {
|
||||
Fmted(FmtedSegments),
|
||||
Float(f64),
|
||||
Int(i64),
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FmtedSegments {
|
||||
pub segments: Vec<FmtSegment>,
|
||||
}
|
||||
|
||||
impl FmtedSegments {
|
||||
pub fn new(segments: Vec<FmtSegment>) -> Self {
|
||||
Self { segments }
|
||||
}
|
||||
|
||||
/// Render the formatted string by stitching together the segments
|
||||
pub fn render_with(&self, dynamic_nodes: Vec<String>) -> String {
|
||||
let mut out = String::new();
|
||||
|
||||
for segment in &self.segments {
|
||||
match segment {
|
||||
FmtSegment::Literal { value } => out.push_str(value),
|
||||
FmtSegment::Dynamic { id } => out.push_str(&dynamic_nodes[*id]),
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Update the segments with new segments
|
||||
///
|
||||
/// this will change how we render the formatted string
|
||||
pub fn update_segments(&mut self, new_segments: Vec<FmtSegment>) {
|
||||
self.segments = new_segments;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use crate::nodes::deserialize_string_leaky;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum FmtSegment {
|
||||
Literal {
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(deserialize_with = "deserialize_string_leaky")
|
||||
)]
|
||||
value: &'static str,
|
||||
},
|
||||
Dynamic {
|
||||
id: usize,
|
||||
},
|
||||
}
|
||||
@@ -27,10 +27,16 @@ mod suspense;
|
||||
mod tasks;
|
||||
mod virtual_dom;
|
||||
|
||||
mod hotreload_utils;
|
||||
|
||||
/// Items exported from this module are used in macros and should not be used directly.
|
||||
#[doc(hidden)]
|
||||
pub mod internal {
|
||||
pub use crate::properties::verify_component_called_as_component;
|
||||
|
||||
pub use crate::hotreload_utils::{
|
||||
FmtSegment, FmtedSegments, HotReloadLiteral, HotreloadedLiteral,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) mod innerlude {
|
||||
@@ -82,13 +88,13 @@ pub mod prelude {
|
||||
provide_context, provide_root_context, queue_effect, remove_future, schedule_update,
|
||||
schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend, throw_error,
|
||||
try_consume_context, use_after_render, use_before_render, use_drop, use_error_boundary,
|
||||
use_hook, use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback, CapturedError,
|
||||
Component, ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event,
|
||||
EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode,
|
||||
OptionStringFromMarker, Properties, ReactiveContext, RenderError, Runtime, RuntimeGuard,
|
||||
ScopeId, ScopeState, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary,
|
||||
SuspenseBoundaryProps, SuspenseContext, SuspenseExtension, Task, Template,
|
||||
TemplateAttribute, TemplateNode, VNode, VNodeInner, VirtualDom,
|
||||
use_hook, use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback, Component,
|
||||
ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event, EventHandler,
|
||||
Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, OptionStringFromMarker,
|
||||
Properties, ReactiveContext, RenderError, RenderReturn, Runtime, RuntimeGuard, ScopeId,
|
||||
ScopeState, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps,
|
||||
SuspenseContext, SuspenseExtension, Task, Template, TemplateAttribute, TemplateNode, VNode,
|
||||
VNodeInner, VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -385,7 +385,7 @@ pub struct Template {
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn deserialize_string_leaky<'a, 'de, D>(deserializer: D) -> Result<&'a str, D::Error>
|
||||
pub(crate) fn deserialize_string_leaky<'a, 'de, D>(deserializer: D) -> Result<&'a str, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
@@ -1076,18 +1076,34 @@ impl IntoAttributeValue for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for f32 {
|
||||
fn into_value(self) -> AttributeValue {
|
||||
AttributeValue::Float(self as _)
|
||||
}
|
||||
}
|
||||
impl IntoAttributeValue for f64 {
|
||||
fn into_value(self) -> AttributeValue {
|
||||
AttributeValue::Float(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for i32 {
|
||||
fn into_value(self) -> AttributeValue {
|
||||
AttributeValue::Int(self as _)
|
||||
}
|
||||
}
|
||||
impl IntoAttributeValue for i64 {
|
||||
fn into_value(self) -> AttributeValue {
|
||||
AttributeValue::Int(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for i128 {
|
||||
fn into_value(self) -> AttributeValue {
|
||||
AttributeValue::Int(self as _)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for bool {
|
||||
fn into_value(self) -> AttributeValue {
|
||||
AttributeValue::Bool(self)
|
||||
@@ -1130,7 +1146,7 @@ pub trait HasAttributes {
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn sort_bfo(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
|
||||
let mut with_indecies = paths.iter().copied().enumerate().collect::<Vec<_>>();
|
||||
with_indecies.sort_unstable_by(|(_, a), (_, b)| {
|
||||
with_indecies.sort_by(|(_, a), (_, b)| {
|
||||
let mut a = a.iter();
|
||||
let mut b = b.iter();
|
||||
loop {
|
||||
|
||||
@@ -164,7 +164,7 @@ pub fn verify_component_called_as_component<C: ComponentFunction<P, M>, P, M>(co
|
||||
diagnostic::on_unimplemented(
|
||||
message = "`Component<{Props}>` is not implemented for `{Self}`",
|
||||
label = "Component",
|
||||
note = "Components are functions in the form `fn() -> Element`, `fn(props: Properties) -> Element`, or `#[component] fn(partial_eq1: u32, partial_eq2: u32) -> Element`.",
|
||||
note = "Components are functions in the form `fn() -> Element`, `fn(props: Properties) -> Element`, or `#[component] fn(partial_eq1: u32, partial_eq2: u32) -> Element`.",
|
||||
note = "You may have forgotten to add `#[component]` to your function to automatically implement the `ComponentFunction` trait."
|
||||
)
|
||||
)]
|
||||
|
||||
@@ -138,6 +138,8 @@ impl ReactiveContext {
|
||||
|
||||
/// Clear all subscribers to this context
|
||||
pub fn clear_subscribers(&self) {
|
||||
// The key type is mutable, but the hash is stable through mutations because we hash by pointer
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let old_subscribers = std::mem::take(&mut self.inner.write().subscribers);
|
||||
for subscriber in old_subscribers {
|
||||
subscriber.0.lock().unwrap().remove(self);
|
||||
@@ -146,6 +148,7 @@ impl ReactiveContext {
|
||||
|
||||
/// Update the subscribers
|
||||
pub(crate) fn update_subscribers(&self) {
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let subscribers = &self.inner.read().subscribers;
|
||||
for subscriber in subscribers.iter() {
|
||||
subscriber.0.lock().unwrap().insert(*self);
|
||||
|
||||
@@ -82,7 +82,6 @@ impl Runtime {
|
||||
{
|
||||
let borrow = self.scope_states.borrow();
|
||||
if let Some(scope) = &borrow[id.0] {
|
||||
let _runtime_guard = RuntimeGuard::new(self.clone());
|
||||
// Manually drop tasks, hooks, and contexts inside of the runtime
|
||||
self.on_scope(id, || {
|
||||
// Drop all spawned tasks - order doesn't matter since tasks don't rely on eachother
|
||||
@@ -112,7 +111,8 @@ impl Runtime {
|
||||
/// Call this function with the current scope set to the given scope
|
||||
///
|
||||
/// Useful in a limited number of scenarios
|
||||
pub fn on_scope<O>(&self, id: ScopeId, f: impl FnOnce() -> O) -> O {
|
||||
pub fn on_scope<O>(self: &Rc<Self>, id: ScopeId, f: impl FnOnce() -> O) -> O {
|
||||
let _runtime_guard = RuntimeGuard::new(self.clone());
|
||||
{
|
||||
self.push_scope(id);
|
||||
}
|
||||
@@ -164,7 +164,7 @@ impl Runtime {
|
||||
|
||||
/// Runs a function with the current runtime
|
||||
pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> Option<R> {
|
||||
RUNTIMES.with(|stack| stack.borrow().last().map(|r| f(r)))
|
||||
Self::current().map(|r| f(&r))
|
||||
}
|
||||
|
||||
/// Runs a function with the current scope
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::{
|
||||
};
|
||||
use crate::{Task, VComponent};
|
||||
use futures_util::StreamExt;
|
||||
use rustc_hash::FxHashMap;
|
||||
use slab::Slab;
|
||||
use std::collections::BTreeSet;
|
||||
use std::{any::Any, rc::Rc};
|
||||
@@ -207,13 +208,8 @@ pub struct VirtualDom {
|
||||
|
||||
pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
|
||||
|
||||
// Maps a template path to a map of byte indexes to templates
|
||||
// if hot reload is enabled, we need to keep track of template overrides
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) templates: rustc_hash::FxHashMap<TemplateId, rustc_hash::FxHashMap<usize, Template>>,
|
||||
// Otherwise, we just need to keep track of what templates we have registered
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub(crate) templates: rustc_hash::FxHashSet<TemplateId>,
|
||||
// A map of overridden templates?
|
||||
pub(crate) templates: FxHashMap<TemplateId, Template>,
|
||||
|
||||
// Templates changes that are queued for the next render
|
||||
pub(crate) queued_templates: Vec<Template>,
|
||||
@@ -576,17 +572,20 @@ impl VirtualDom {
|
||||
// we only replace templates if hot reloading is enabled
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.register_template_first_byte_index(template);
|
||||
// Save the template ID
|
||||
self.templates.insert(template.name, template);
|
||||
|
||||
// Only queue the template to be written if its not completely dynamic
|
||||
if !template.is_completely_dynamic() {
|
||||
self.queued_templates.push(template);
|
||||
}
|
||||
|
||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||
let mut dirty = Vec::new();
|
||||
for (id, scope) in self.scopes.iter() {
|
||||
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
|
||||
fn check_node_for_templates(node: &crate::VNode, template: Template) -> bool {
|
||||
let this_template_name = node.template.get().name.rsplit_once(':').unwrap().0;
|
||||
let other_template_name = template.name.rsplit_once(':').unwrap().0;
|
||||
|
||||
if this_template_name == other_template_name {
|
||||
if node.template.get().name == template.name {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user