mod.rs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. use crate::{cfg::ConfigOptsServe, BuildResult, Result};
  2. use dioxus_cli_config::CrateConfig;
  3. use cargo_metadata::diagnostic::Diagnostic;
  4. use dioxus_core::Template;
  5. use dioxus_hot_reload::HotReloadMsg;
  6. use dioxus_html::HtmlCtx;
  7. use dioxus_rsx::hot_reload::*;
  8. use fs_extra::{dir::CopyOptions, file};
  9. use notify::{RecommendedWatcher, Watcher};
  10. use std::{
  11. path::PathBuf,
  12. sync::{Arc, Mutex},
  13. };
  14. use tokio::sync::broadcast::{self};
  15. mod output;
  16. use output::*;
  17. pub mod desktop;
  18. pub mod fullstack;
  19. pub mod web;
  20. #[derive(Clone)]
  21. pub struct HotReloadState {
  22. pub messages: broadcast::Sender<HotReloadMsg>,
  23. pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
  24. }
  25. /// Sets up a file watcher.
  26. ///
  27. /// Will attempt to hotreload HTML, RSX (.rs), and CSS
  28. async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
  29. build_with: F,
  30. config: &CrateConfig,
  31. web_info: Option<WebServerInfo>,
  32. hot_reload: Option<HotReloadState>,
  33. ) -> Result<RecommendedWatcher> {
  34. let mut last_update_time = chrono::Local::now().timestamp();
  35. // file watcher: check file change
  36. let mut allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
  37. // Extend the watch path to include the assets directory
  38. allow_watch_path.push(config.dioxus_config.application.asset_dir.clone());
  39. // Create the file watcher
  40. let mut watcher = notify::recommended_watcher({
  41. let watcher_config = config.clone();
  42. move |info: notify::Result<notify::Event>| {
  43. let Ok(e) = info else {
  44. return;
  45. };
  46. watch_event(
  47. e,
  48. &mut last_update_time,
  49. &hot_reload,
  50. &watcher_config,
  51. &build_with,
  52. &web_info,
  53. );
  54. }
  55. })
  56. .expect("Failed to create file watcher - please ensure you have the required permissions to watch the specified directories.");
  57. // Watch the specified paths
  58. for sub_path in allow_watch_path {
  59. let path = &config.crate_dir.join(sub_path);
  60. let mode = notify::RecursiveMode::Recursive;
  61. if let Err(err) = watcher.watch(path, mode) {
  62. log::warn!("Failed to watch path: {}", err);
  63. }
  64. }
  65. Ok(watcher)
  66. }
  67. fn watch_event<F>(
  68. event: notify::Event,
  69. last_update_time: &mut i64,
  70. hot_reload: &Option<HotReloadState>,
  71. config: &CrateConfig,
  72. build_with: &F,
  73. web_info: &Option<WebServerInfo>,
  74. ) where
  75. F: Fn() -> Result<BuildResult> + Send + 'static,
  76. {
  77. // Ensure that we're tracking only modifications
  78. if !matches!(
  79. event.kind,
  80. notify::EventKind::Create(_) | notify::EventKind::Remove(_) | notify::EventKind::Modify(_)
  81. ) {
  82. return;
  83. }
  84. // Ensure that we're not rebuilding too frequently
  85. if chrono::Local::now().timestamp() <= *last_update_time {
  86. return;
  87. }
  88. // By default we want to opt into a full rebuild, but hotreloading will actually set this force us
  89. let mut needs_full_rebuild = true;
  90. if let Some(hot_reload) = &hot_reload {
  91. hotreload_files(hot_reload, &mut needs_full_rebuild, &event, &config);
  92. }
  93. if needs_full_rebuild {
  94. full_rebuild(build_with, last_update_time, config, event, web_info);
  95. }
  96. }
  97. fn full_rebuild<F>(
  98. build_with: &F,
  99. last_update_time: &mut i64,
  100. config: &CrateConfig,
  101. event: notify::Event,
  102. web_info: &Option<WebServerInfo>,
  103. ) where
  104. F: Fn() -> Result<BuildResult> + Send + 'static,
  105. {
  106. match build_with() {
  107. Ok(res) => {
  108. *last_update_time = chrono::Local::now().timestamp();
  109. #[allow(clippy::redundant_clone)]
  110. print_console_info(
  111. &config,
  112. PrettierOptions {
  113. changed: event.paths.clone(),
  114. warnings: res.warnings,
  115. elapsed_time: res.elapsed_time,
  116. },
  117. web_info.clone(),
  118. );
  119. }
  120. Err(e) => {
  121. *last_update_time = chrono::Local::now().timestamp();
  122. log::error!("{:?}", e);
  123. }
  124. }
  125. }
  126. fn hotreload_files(
  127. hot_reload: &HotReloadState,
  128. needs_full_rebuild: &mut bool,
  129. event: &notify::Event,
  130. config: &CrateConfig,
  131. ) {
  132. // find changes to the rsx in the file
  133. let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
  134. let mut messages: Vec<HotReloadMsg> = Vec::new();
  135. // In hot reload mode, we only need to rebuild if non-rsx code is changed
  136. *needs_full_rebuild = false;
  137. for path in &event.paths {
  138. // for various assets that might be linked in, we just try to hotreloading them forcefully
  139. // That is, unless they appear in an include! macro, in which case we need to a full rebuild....
  140. let Some(ext) = path.extension().and_then(|v| v.to_str()) else {
  141. continue;
  142. };
  143. // Workaround for notify and vscode-like editor:
  144. // when edit & save a file in vscode, there will be two notifications,
  145. // the first one is a file with empty content.
  146. // filter the empty file notification to avoid false rebuild during hot-reload
  147. if let Ok(metadata) = fs::metadata(path) {
  148. if metadata.len() == 0 {
  149. continue;
  150. }
  151. }
  152. match ext {
  153. // Attempt hot reload
  154. "rs" => {}
  155. // Anything with a .file is also ignored
  156. _ if path.file_stem().is_none() || ext.ends_with("~") => {}
  157. // Anything else is a maybe important file that needs to be rebuilt
  158. _ => {
  159. // If it happens to be a file in the asset directory, there's a chance we can hotreload it.
  160. // Only css is currently supported for hotreload
  161. if ext == "css" {
  162. let asset_dir = config
  163. .crate_dir
  164. .join(&config.dioxus_config.application.asset_dir);
  165. if path.starts_with(&asset_dir) {
  166. let local_path: PathBuf = path
  167. .file_name()
  168. .unwrap()
  169. .to_str()
  170. .unwrap()
  171. .to_string()
  172. .parse()
  173. .unwrap();
  174. println!(
  175. "maybe tracking asset: {:?}, {:#?}",
  176. local_path,
  177. rsx_file_map.tracked_assets()
  178. );
  179. if let Some(f) = rsx_file_map.is_tracking_asset(&local_path) {
  180. println!(
  181. "Hot reloading asset - it's tracked by the rsx!: {:?}",
  182. local_path
  183. );
  184. // copy the asset over tothe output directory
  185. let output_dir = config.out_dir();
  186. fs_extra::copy_items(
  187. &[path],
  188. output_dir,
  189. &CopyOptions::new().overwrite(true),
  190. )
  191. .unwrap();
  192. messages.push(HotReloadMsg::UpdateAsset(local_path));
  193. continue;
  194. }
  195. }
  196. }
  197. *needs_full_rebuild = true;
  198. }
  199. };
  200. match rsx_file_map.update_rsx(path, &config.crate_dir) {
  201. Ok(UpdateResult::UpdatedRsx(msgs)) => {
  202. println!("Updated: {:?}", msgs);
  203. messages.extend(
  204. msgs.into_iter()
  205. .map(|msg| HotReloadMsg::UpdateTemplate(msg)),
  206. );
  207. *needs_full_rebuild = false;
  208. }
  209. Ok(UpdateResult::NeedsRebuild) => {
  210. *needs_full_rebuild = true;
  211. }
  212. Err(err) => {
  213. log::error!("{}", err);
  214. }
  215. }
  216. }
  217. // If full rebuild, extend the file map with the new file map
  218. // This will wipe away any previous cached changed templates
  219. if *needs_full_rebuild {
  220. // Reset the file map to the new state of the project
  221. let FileMapBuildResult {
  222. map: new_file_map,
  223. errors,
  224. } = FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
  225. for err in errors {
  226. log::error!("{}", err);
  227. }
  228. *rsx_file_map = new_file_map;
  229. return;
  230. }
  231. println!("Hot reloading: {:?}", messages);
  232. for msg in messages {
  233. let _ = hot_reload.messages.send(msg);
  234. }
  235. }
  236. pub(crate) trait Platform {
  237. fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
  238. where
  239. Self: Sized;
  240. fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult>;
  241. }
  242. // Some("bin") => "application/octet-stream",
  243. // Some("css") => "text/css",
  244. // Some("csv") => "text/csv",
  245. // Some("html") => "text/html",
  246. // Some("ico") => "image/vnd.microsoft.icon",
  247. // Some("js") => "text/javascript",
  248. // Some("json") => "application/json",
  249. // Some("jsonld") => "application/ld+json",
  250. // Some("mjs") => "text/javascript",
  251. // Some("rtf") => "application/rtf",
  252. // Some("svg") => "image/svg+xml",
  253. // Some("mp4") => "video/mp4",