use crate::{cfg::ConfigOptsServe, BuildResult, Result};
use dioxus_cli_config::CrateConfig;
use cargo_metadata::diagnostic::Diagnostic;
use dioxus_core::Template;
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use fs_extra::dir::CopyOptions;
use notify::{RecommendedWatcher, Watcher};
use std::{path::PathBuf, sync::Arc};
use tokio::sync::broadcast::{self};
mod output;
use output::*;
pub mod desktop;
pub mod fullstack;
pub mod web;
#[derive(Clone)]
pub struct HotReloadState {
pub messages: broadcast::Sender<HotReloadMsg>,
pub file_map: SharedFileMap,
}
type SharedFileMap = Arc<Mutex<FileMap<HtmlCtx>>>;
impl HotReloadState {
pub fn all_templates(&self) -> Vec<Template> {
self.file_map
.lock()
.unwrap()
.map
.values()
.flat_map(|v| v.templates.values().copied())
.collect()
}
}
async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
build_with: F,
config: &CrateConfig,
web_info: Option<WebServerInfo>,
hot_reload: Option<HotReloadState>,
) -> Result<RecommendedWatcher> {
let mut last_update_time = chrono::Local::now().timestamp();
let mut allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
allow_watch_path.push(config.dioxus_config.application.asset_dir.clone());
allow_watch_path.push("Cargo.toml".to_string().into());
allow_watch_path.push("Dioxus.toml".to_string().into());
allow_watch_path.dedup();
let mut watcher = notify::recommended_watcher({
let watcher_config = config.clone();
move |info: notify::Result<notify::Event>| {
let Ok(e) = info else {
return;
};
watch_event(
e,
&mut last_update_time,
&hot_reload,
&watcher_config,
&build_with,
&web_info,
);
}
})
.expect("Failed to create file watcher - please ensure you have the required permissions to watch the specified directories.");
for sub_path in allow_watch_path {
let path = &config.crate_dir.join(sub_path);
let mode = notify::RecursiveMode::Recursive;
if let Err(err) = watcher.watch(path, mode) {
tracing::warn!("Failed to watch path: {}", err);
}
}
Ok(watcher)
}
fn watch_event<F>(
event: notify::Event,
last_update_time: &mut i64,
hot_reload: &Option<HotReloadState>,
config: &CrateConfig,
build_with: &F,
web_info: &Option<WebServerInfo>,
) where
F: Fn() -> Result<BuildResult> + Send + 'static,
{
if !matches!(
event.kind,
notify::EventKind::Create(_) | notify::EventKind::Remove(_) | notify::EventKind::Modify(_)
) {
return;
}
if chrono::Local::now().timestamp() <= *last_update_time {
return;
}
let mut needs_full_rebuild = false;
if let Some(hot_reload) = &hot_reload {
hotreload_files(hot_reload, &mut needs_full_rebuild, &event, config);
}
if needs_full_rebuild {
full_rebuild(build_with, last_update_time, config, event, web_info);
}
}
fn full_rebuild<F>(
build_with: &F,
last_update_time: &mut i64,
config: &CrateConfig,
event: notify::Event,
web_info: &Option<WebServerInfo>,
) where
F: Fn() -> Result<BuildResult> + Send + 'static,
{
match build_with() {
Ok(res) => {
*last_update_time = chrono::Local::now().timestamp();
#[allow(clippy::redundant_clone)]
print_console_info(
config,
PrettierOptions {
changed: event.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
}
Err(e) => {
*last_update_time = chrono::Local::now().timestamp();
tracing::error!("{:?}", e);
}
}
}
fn hotreload_files(
hot_reload: &HotReloadState,
needs_full_rebuild: &mut bool,
event: ¬ify::Event,
config: &CrateConfig,
) {
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
let mut messages: Vec<HotReloadMsg> = Vec::new();
for path in &event.paths {
let is_potentially_reloadable = hotreload_file(
path,
config,
&rsx_file_map,
&mut messages,
needs_full_rebuild,
);
if is_potentially_reloadable.is_none() {
continue;
}
match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(msgs.into_iter().map(HotReloadMsg::UpdateTemplate));
}
Ok(UpdateResult::NeedsRebuild) => {
tracing::trace!("Needs full rebuild because file changed: {:?}", path);
*needs_full_rebuild = true;
}
Err(err) => tracing::error!("{}", err),
}
}
if *needs_full_rebuild {
let FileMapBuildResult {
map: new_file_map,
errors,
} = FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
tracing::error!("{}", err);
}
*rsx_file_map = new_file_map;
return;
}
for msg in messages {
let _ = hot_reload.messages.send(msg);
}
}
fn hotreload_file(
path: &Path,
config: &CrateConfig,
rsx_file_map: &std::sync::MutexGuard<'_, FileMap<HtmlCtx>>,
messages: &mut Vec<HotReloadMsg>,
needs_full_rebuild: &mut bool,
) -> Option<()> {
let ext = path.extension().and_then(|v| v.to_str())?;
if let Ok(metadata) = fs::metadata(path) {
if metadata.len() == 0 {
return None;
}
}
if is_backup_file(path) {
tracing::trace!("Ignoring backup file: {:?}", path);
return None;
}
if ext == "css" {
let asset_dir = config
.crate_dir
.join(&config.dioxus_config.application.asset_dir);
if attempt_css_reload(path, asset_dir, rsx_file_map, config, messages).is_none() {
*needs_full_rebuild = true;
}
return None;
}
if ext != "rs" && ext != "css" {
*needs_full_rebuild = true;
return None;
}
Some(())
}
fn attempt_css_reload(
path: &Path,
asset_dir: PathBuf,
rsx_file_map: &std::sync::MutexGuard<'_, FileMap<HtmlCtx>>,
config: &CrateConfig,
messages: &mut Vec<HotReloadMsg>,
) -> Option<()> {
if !path.starts_with(asset_dir) {
return None;
}
let local_path = local_path_of_asset(path)?;
_ = rsx_file_map.is_tracking_asset(&local_path)?;
_ = fs_extra::copy_items(
&[path],
config.out_dir(),
&CopyOptions::new().overwrite(true),
);
messages.push(HotReloadMsg::UpdateAsset(local_path));
Some(())
}
fn local_path_of_asset(path: &Path) -> Option<PathBuf> {
path.file_name()?.to_str()?.to_string().parse().ok()
}
pub(crate) trait Platform {
fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
where
Self: Sized;
fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult>;
}
fn is_backup_file(path: &Path) -> bool {
if let Some(name) = path.file_name() {
if let Some(name) = name.to_str() {
if name.ends_with('~') {
return true;
}
}
}
if let Some(name) = path.file_name() {
if let Some(name) = name.to_str() {
if name.starts_with('.') {
return true;
}
}
}
false
}
#[test]
fn test_is_backup_file() {
assert!(is_backup_file(&PathBuf::from("examples/test.rs~")));
assert!(is_backup_file(&PathBuf::from("examples/.back")));
assert!(is_backup_file(&PathBuf::from("test.rs~")));
assert!(is_backup_file(&PathBuf::from(".back")));
assert!(!is_backup_file(&PathBuf::from("val.rs")));
assert!(!is_backup_file(&PathBuf::from(
"/Users/jonkelley/Development/Tinkering/basic_05_example/src/lib.rs"
)));
assert!(!is_backup_file(&PathBuf::from("exmaples/val.rs")));
}