glory_hot_reload/
lib.rs

1extern crate proc_macro;
2
3use anyhow::Result;
4use camino::Utf8PathBuf;
5use diff::Patches;
6use node::LNode;
7use parking_lot::RwLock;
8use serde::{Deserialize, Serialize};
9use std::{
10    collections::HashMap,
11    fs::File,
12    io::Read,
13    path::{Path, PathBuf},
14    sync::Arc,
15};
16use syn::{
17    spanned::Spanned,
18    visit::{self, Visit},
19    Macro,
20};
21use walkdir::WalkDir;
22
23pub mod diff;
24pub mod node;
25pub mod parsing;
26
27pub const HOT_RELOAD_JS: &str = include_str!("patch.js");
28
29#[derive(Debug, Clone, Default)]
30pub struct ViewMacros {
31    // keyed by original location identifier
32    views: Arc<RwLock<HashMap<Utf8PathBuf, Vec<MacroInvocation>>>>,
33}
34
35impl ViewMacros {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    pub fn update_from_paths<T: AsRef<Path>>(&self, paths: &[T]) -> Result<()> {
41        let mut views = HashMap::new();
42
43        for path in paths {
44            for entry in WalkDir::new(path).into_iter().flatten() {
45                if entry.file_type().is_file() {
46                    let path: PathBuf = entry.path().into();
47                    let path = Utf8PathBuf::try_from(path)?;
48                    if path.extension() == Some("rs") || path.ends_with(".rs") {
49                        let macros = Self::parse_file(&path)?;
50                        let entry = views.entry(path.clone()).or_default();
51                        *entry = macros;
52                    }
53                }
54            }
55        }
56
57        *self.views.write() = views;
58
59        Ok(())
60    }
61
62    pub fn parse_file(path: &Utf8PathBuf) -> Result<Vec<MacroInvocation>> {
63        let mut file = File::open(path)?;
64        let mut content = String::new();
65        file.read_to_string(&mut content)?;
66        let ast = syn::parse_file(&content)?;
67
68        let mut visitor = ViewMacroVisitor::default();
69        visitor.visit_file(&ast);
70        let mut views = Vec::new();
71        for view in visitor.views {
72            let span = view.span();
73            let id = span_to_stable_id(path, span.start().line);
74            let tokens = view.tokens.clone().into_iter();
75            // TODO handle class = ...
76            let rsx = rstml::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
77            let template = LNode::parse_view(rsx)?;
78            views.push(MacroInvocation { id, template })
79        }
80        Ok(views)
81    }
82
83    pub fn patch(&self, path: &Utf8PathBuf) -> Result<Option<Patches>> {
84        let new_views = Self::parse_file(path)?;
85        let mut lock = self.views.write();
86        let diffs = match lock.get(path) {
87            None => return Ok(None),
88            Some(current_views) => {
89                if current_views.len() == new_views.len() {
90                    let mut diffs = Vec::new();
91                    for (current_view, new_view) in current_views.iter().zip(&new_views) {
92                        if current_view.id == new_view.id && current_view.template != new_view.template {
93                            diffs.push((current_view.id.clone(), current_view.template.diff(&new_view.template)));
94                        }
95                    }
96                    diffs
97                } else {
98                    return Ok(None);
99                }
100            }
101        };
102
103        // update the status to the new views
104        lock.insert(path.clone(), new_views);
105
106        Ok(Some(Patches(diffs)))
107    }
108}
109
110#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
111pub struct MacroInvocation {
112    id: String,
113    template: LNode,
114}
115
116impl std::fmt::Debug for MacroInvocation {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        f.debug_struct("MacroInvocation").field("id", &self.id).finish()
119    }
120}
121
122#[derive(Default, Debug)]
123pub struct ViewMacroVisitor<'a> {
124    views: Vec<&'a Macro>,
125}
126
127impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
128    fn visit_macro(&mut self, node: &'ast Macro) {
129        let ident = node.path.get_ident().map(|n| n.to_string());
130        if ident == Some("view".to_string()) {
131            self.views.push(node);
132        }
133
134        // Delegate to the default impl to visit any nested functions.
135        visit::visit_macro(self, node);
136    }
137}
138
139pub fn span_to_stable_id(path: impl AsRef<Path>, line: usize) -> String {
140    let file = path.as_ref().to_str().unwrap_or_default().replace(['/', '\\'], "-");
141    format!("{file}-{line}")
142}