Files
dioxus/packages/cli/src/serve/hot_reloading_file_map.rs
T
88af3e7eff 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>
2024-07-17 19:11:18 -07:00

314 lines
11 KiB
Rust

use dioxus_core::{internal::HotReloadLiteral, Template};
use dioxus_rsx::{
hot_reload::{diff_rsx, ChangedRsx},
CallBody, HotReloadedTemplate, HotReloadingContext,
};
use krates::cm::MetadataCommand;
use krates::Cmd;
pub use std::collections::HashMap;
use std::{ffi::OsStr, path::PathBuf};
pub use std::{fs, io, path::Path};
pub use std::{fs::File, io::Read};
use syn::spanned::Spanned;
pub struct FileMap {
pub map: HashMap<PathBuf, CachedSynFile>,
/// Any errors that occurred while building the FileMap that were not fatal
pub errors: Vec<io::Error>,
pub in_workspace: HashMap<PathBuf, Option<PathBuf>>,
pub changed_lits: HashMap<String, HotReloadLiteral>,
}
/// A cached file that has been parsed
///
/// We store the templates found in this file
pub struct CachedSynFile {
pub raw: String,
pub templates: HashMap<&'static str, Template>,
}
impl FileMap {
/// Create a new FileMap from a crate directory
///
/// TODO: this should be created with a gitignore filter
pub fn create<Ctx: HotReloadingContext>(path: PathBuf) -> io::Result<FileMap> {
Self::create_with_filter::<Ctx>(path, |p| {
// skip some stuff we know is large by default
p.file_name() == Some(OsStr::new("target"))
|| p.file_name() == Some(OsStr::new("node_modules"))
})
}
/// Create a new FileMap from a crate directory
///
/// Takes a filter that when returns true, the file will be filtered out (ie not tracked)
/// Note that this is inverted from a typical .filter() method.
pub fn create_with_filter<Ctx: HotReloadingContext>(
crate_dir: PathBuf,
mut filter: impl FnMut(&Path) -> bool,
) -> io::Result<FileMap> {
let FileMapSearchResult { map, errors } = find_rs_files(crate_dir.clone(), &mut filter);
let mut map = Self {
map,
errors,
in_workspace: HashMap::new(),
changed_lits: HashMap::new(),
};
map.load_assets::<Ctx>(crate_dir.as_path());
Ok(map)
}
/// Start watching assets for changes
///
/// This just diffs every file against itself and populates the tracked assets as it goes
pub fn load_assets<Ctx: HotReloadingContext>(&mut self, crate_dir: &Path) {
let keys = self.map.keys().cloned().collect::<Vec<_>>();
for file in keys {
_ = self.update_rsx::<Ctx>(file.as_path(), crate_dir);
}
}
/// Try to update the rsx in a file
pub fn update_rsx<Ctx: HotReloadingContext>(
&mut self,
file_path: &Path,
crate_dir: &Path,
) -> Result<Vec<HotReloadedTemplate>, HotreloadError> {
let src = std::fs::read_to_string(file_path)?;
// If we can't parse the contents we want to pass it off to the build system to tell the user that there's a syntax error
let syntax = syn::parse_file(&src).map_err(|_err| HotreloadError::Parse)?;
let in_workspace = self.child_in_workspace(crate_dir)?;
// Get the cached file if it exists, otherwise try to create it
let Some(old_cached) = self.map.get_mut(file_path) else {
// if this is a new file, rebuild the project
let mut map = FileMap::create::<Ctx>(crate_dir.to_path_buf())?;
if let Some(err) = map.errors.pop() {
return Err(HotreloadError::Failure(err));
}
// merge the new map into the old map
self.map.extend(map.map);
return Err(HotreloadError::Notreloadable);
};
// If the cached file is not a valid rsx file, rebuild the project, forcing errors
// TODO: in theory the error is simply in the RsxCallbody. We could attempt to parse it using partial expansion
// And collect out its errors instead of giving up to a full rebuild
let old = syn::parse_file(&old_cached.raw).map_err(|_e| HotreloadError::Parse)?;
let instances = match diff_rsx(&syntax, &old) {
// If the changes were just some rsx, we can just update the template
//
// However... if the changes involved code in the rsx itself, this should actually be a CodeChanged
Some(rsx_calls) => rsx_calls,
// If the changes were some code, we should insert the file into the map and rebuild
// todo: not sure we even need to put the cached file into the map, but whatever
None => {
let cached_file = CachedSynFile {
raw: src.clone(),
templates: HashMap::new(),
};
self.map.insert(file_path.to_path_buf(), cached_file);
return Err(HotreloadError::Notreloadable);
}
};
let mut out_templates = vec![];
for calls in instances.into_iter() {
let ChangedRsx { old, new } = calls;
let old_start = old.span().start();
let old_parsed = syn::parse2::<CallBody>(old.tokens);
let new_parsed = syn::parse2::<CallBody>(new.tokens);
let (Ok(old_call_body), Ok(new_call_body)) = (old_parsed, new_parsed) else {
continue;
};
// if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root
// we need to check if the file is in a workspace or not and strip the prefix accordingly
let prefix = match in_workspace {
Some(ref workspace) => workspace,
_ => crate_dir,
};
let Ok(file) = file_path.strip_prefix(prefix) else {
continue;
};
// We leak the template since templates are a compiletime value
// This is not ideal, but also not a huge deal for hot reloading
// TODO: we could consider arena allocating the templates and dropping them when the connection is closed
let leaked_location = Box::leak(template_location(old_start, file).into_boxed_str());
// Returns a list of templates that are hotreloadable
let hotreload_result = dioxus_rsx::hotreload::HotReloadedTemplate::new::<Ctx>(
&old_call_body,
&new_call_body,
leaked_location,
self.changed_lits.clone(),
);
// if the template is not hotreloadable, we need to do a full rebuild
let Some(mut results) = hotreload_result else {
return Err(HotreloadError::Notreloadable);
};
// self.changed_lits
// .extend(std::mem::take(&mut results.changed_lits));
// Be careful to not send the bad templates
results.templates.retain(|template| {
// dioxus cannot handle empty templates...
if template.roots.is_empty() {
return false;
}
// if the template is the same, don't send it
if old_cached.templates.get(template.name) == Some(template) {
return false;
};
// Update the most recent idea of the template
// This lets us know if the template has changed so we don't need to send it
old_cached.templates.insert(template.name, *template);
true
});
out_templates.push(results);
}
Ok(out_templates)
}
fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result<Option<PathBuf>> {
if let Some(in_workspace) = self.in_workspace.get(crate_dir) {
return Ok(in_workspace.clone());
}
let mut cmd = Cmd::new();
let manafest_path = crate_dir.join("Cargo.toml");
cmd.manifest_path(&manafest_path);
let cmd: MetadataCommand = cmd.into();
let metadata = cmd
.exec()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let in_workspace = metadata.workspace_root != crate_dir;
let workspace_path = in_workspace.then(|| metadata.workspace_root.into());
self.in_workspace
.insert(crate_dir.to_path_buf(), workspace_path.clone());
Ok(workspace_path)
}
}
pub fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> String {
let line = old_start.line;
let column = old_start.column + 1;
// Always ensure the path components are separated by `/`.
let path = file
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/");
path
+ ":"
+ line.to_string().as_str()
+ ":"
+ column.to_string().as_str()
// the byte index doesn't matter, but dioxus needs it
+ ":0"
}
struct FileMapSearchResult {
map: HashMap<PathBuf, CachedSynFile>,
errors: Vec<io::Error>,
}
// todo: we could just steal the mod logic from rustc itself
fn find_rs_files(root: PathBuf, filter: &mut impl FnMut(&Path) -> bool) -> FileMapSearchResult {
let mut files = HashMap::new();
let mut errors = Vec::new();
if root.is_dir() {
let read_dir = match fs::read_dir(root) {
Ok(read_dir) => read_dir,
Err(err) => {
errors.push(err);
return FileMapSearchResult { map: files, errors };
}
};
for entry in read_dir.flatten() {
let path = entry.path();
if !filter(&path) {
let FileMapSearchResult {
map,
errors: child_errors,
} = find_rs_files(path, filter);
errors.extend(child_errors);
files.extend(map);
}
}
} else if root.extension().and_then(|s| s.to_str()) == Some("rs") {
if let Ok(mut file) = File::open(root.clone()) {
let mut src = String::new();
match file.read_to_string(&mut src) {
Ok(_) => {
let cached_file = CachedSynFile {
raw: src.clone(),
templates: HashMap::new(),
};
// track assets while we're here
files.insert(root, cached_file);
}
Err(err) => {
errors.push(err);
}
}
}
}
FileMapSearchResult { map: files, errors }
}
#[derive(Debug)]
pub enum HotreloadError {
Failure(io::Error),
Parse,
Notreloadable,
}
impl std::fmt::Display for HotreloadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Failure(err) => write!(f, "Failed to parse file: {}", err),
Self::Parse => write!(f, "Failed to parse file"),
Self::Notreloadable => write!(f, "Template is not hotreloadable"),
}
}
}
impl From<io::Error> for HotreloadError {
fn from(err: io::Error) -> Self {
HotreloadError::Failure(err)
}
}